diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 61f8cf4..8616e4d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,7 +4,7 @@ - Xcode version: - Swift version: - macOS version running Xcode: -- Dependency manager (SPM, Carthage, CocoaPods, Manually): +- Dependency manager (SPM, Manually): ## What did you do? diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2aa11af..8837ce0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,19 +1,21 @@ name: CI -on: [push] +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop jobs: - tests: - name: Unit-Tests - runs-on: macOS-latest + build: + runs-on: macos-latest steps: - - uses: actions/checkout@v1 - - name: fastlane ios tests - run: fastlane ios tests - compatibility: - name: Compatibility-Tests - runs-on: macOS-latest - steps: - - uses: actions/checkout@v1 - - name: fastlane ios compatibilityTests - run: fastlane ios compatibilityTests + - uses: actions/checkout@v2 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v diff --git a/.gitignore b/.gitignore index 504ac79..11f1ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,7 @@ - -# Created by https://www.gitignore.io/api/macos,xcode,carthage,cocoapods,fastlane - -### Carthage ### -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -Carthage/Checkouts - -Carthage/Build - -### CocoaPods ### -## CocoaPods GitIgnore Template - -# CocoaPods - Only use to conserve bandwidth / Save time on Pushing -# - Also handy if you have a large number of dependant pods -# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE -Pods/ - -### macOS ### -*.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Documentation -Documentation - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Xcode ### -# 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 -*.xccheckout -*.xcscmblueprint - -### Xcode Patch ### -*.xcodeproj/* -!*.xcodeproj/project.pbxproj -!*.xcodeproj/xcshareddata/ -!*.xcworkspace/contents.xcworkspacedata -/*.gcno - -### fastlane ### -# fastlane - A streamlined workflow tool for Cocoa deployment -# -# 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://docs.fastlane.tools/best-practices/source-control/#source-control - -# fastlane specific -/fastlane/README.md -fastlane/report.xml - -# deliver temporary files -fastlane/Preview.html - -# snapshot generated screenshots -fastlane/screenshots/**/*.png -fastlane/screenshots/screenshots.html - -# scan temporary files -fastlane/test_output - -# End of https://www.gitignore.io/api/macos,xcode,carthage,cocoapods,fastlane \ No newline at end of file +DerivedData/ +.swiftpm/ \ No newline at end of file diff --git a/.jazzy.yaml b/.jazzy.yaml deleted file mode 100644 index a5ea04b..0000000 --- a/.jazzy.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# WhatsNewKit document generator jazzy settings - -copyright: Copyright 2020 Sven Tiigi -author: Sven Tiigi -github_url: https://github.com/SvenTiigi/WhatsNewKit -xcodebuild_arguments: [-target, WhatsNewKit-iOS] -clean: true -output: ./Documentation diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index e6206ce..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,26 +0,0 @@ -opt_in_rules: # some rules are only opt-in - - empty_count - - redundant_nil_coalescing - - switch_case_on_newline - - force_unwrapping - - conditional_returns_on_newline - - closure_spacing - - implicitly_unwrapped_optional - - sorted_imports - - valid_docs - -included: - - Sources - -excluded: # paths to ignore during linting. Takes precedence over `included`. - - Tests - - Example - -disabled_rules: # rule identifiers to exclude from running - - trailing_whitespace - - identifier_name - -line_length: 130 -file_length: 550 -function_body_length: 50 -cyclomatic_complexity: 13 diff --git a/Configs/WhatsNewKit.plist b/Configs/WhatsNewKit.plist deleted file mode 100644 index be2d93b..0000000 --- a/Configs/WhatsNewKit.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.3.7 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSHumanReadableCopyright - Copyright © 2018 Sven Tiigi. All rights reserved. - NSPrincipalClass - - - diff --git a/Configs/WhatsNewKitTests.plist b/Configs/WhatsNewKitTests.plist deleted file mode 100644 index 40c54a1..0000000 --- a/Configs/WhatsNewKitTests.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.3.7 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift deleted file mode 100644 index abea8da..0000000 --- a/Example/AppDelegate.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// AppDelegate.swift -// Example -// -// Created by Sven Tiigi on 18.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import WhatsNewKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - /// The UIWindow - var window: UIWindow? - - /// The UINavigationController with ViewController as root viewcontroller - lazy var navigationController: UINavigationController = { - let navigationController = UINavigationController(rootViewController: ExampleViewController()) - navigationController.navigationBar.prefersLargeTitles = true - navigationController.navigationBar.largeTitleTextAttributes = [ - .foregroundColor: UIColor.main - ] - navigationController.view.backgroundColor = .white - navigationController.navigationBar.tintColor = .main - navigationController.navigationBar.titleTextAttributes = [ - .foregroundColor: UIColor.main - ] - return navigationController - }() - - /// Main Entry Point - /// - /// - Parameters: - /// - application: The Application - /// - launchOptions: The LaunchOptions - /// - Returns: Boolean - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - // Initialize Window - self.window = UIWindow(frame: UIScreen.main.bounds) - // Set root viewcontroller - self.window?.rootViewController = self.navigationController - // Make key and visible - self.window?.makeKeyAndVisible() - return true - } - -} - diff --git a/Example/Configurations/AnimationConfiguration.swift b/Example/Configurations/AnimationConfiguration.swift deleted file mode 100644 index 742a969..0000000 --- a/Example/Configurations/AnimationConfiguration.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// AnimationConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The AnimationConfiguration -class AnimationConfiguration: Configuration { - - /// The Title - let title: String = "Animation 🎬" - - /// The Subtitle - let subtitle: String = "Select an animation" - - /// The Options - let options = [ - "None", - "Fade", - "Slide\nUp", - "Slide\nDown", - "Slide\nLeft", - "Slide\nRight" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - let animation: WhatsNewViewController.Animation - switch self.selectedIndex { - case 0: - return - case 1: - animation = .fade - case 2: - animation = .slideUp - case 3: - animation = .slideDown - case 4: - animation = .slideLeft - case 5: - animation = .slideRight - default: - return - } - configuration.apply(animation: animation) - } - -} diff --git a/Example/Configurations/BackgroundColorConfiguration.swift b/Example/Configurations/BackgroundColorConfiguration.swift deleted file mode 100644 index f5f6192..0000000 --- a/Example/Configurations/BackgroundColorConfiguration.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// BackgroundColorConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The BackgroundColorConfiguration -class BackgroundColorConfiguration: Configuration { - - /// The Title - let title: String = "BackgroundColor 🌄" - - /// The Subtitle - let subtitle: String = "Choose a background color" - - /// The Options - let options = [ - "White", - "Dark" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if selectedIndex == 0 { - configuration.apply(theme: .whiteBlue) - } else { - configuration.apply(theme: .darkBlue) - } - } - -} diff --git a/Example/Configurations/Configuration.swift b/Example/Configurations/Configuration.swift deleted file mode 100644 index 0f1c8c1..0000000 --- a/Example/Configurations/Configuration.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Configuration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The Configuration -protocol Configuration { - - /// The Title - var title: String { get } - - /// The Subtitle - var subtitle: String { get } - - /// The Options - var options: [String] { get } - - /// The selected Index - var selectedIndex: Int { get set } - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) - -} diff --git a/Example/Configurations/ContentModeConfiguration.swift b/Example/Configurations/ContentModeConfiguration.swift deleted file mode 100644 index c9a514e..0000000 --- a/Example/Configurations/ContentModeConfiguration.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ContentModeConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 10.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The ContentModeConfiguration -class ContentModeConfiguration: Configuration { - - /// The Title - let title: String = "ContentMode 📏" - - /// The Subtitle - let subtitle: String = "Configure the ContentMode" - - /// The Options - let options = [ - "Top", - "Center", - "Fill" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - configuration.itemsView.contentMode = .top - } else if self.selectedIndex == 1 { - configuration.itemsView.contentMode = .center - } else { - configuration.itemsView.contentMode = .fill - } - } - -} diff --git a/Example/Configurations/HapticFeedbackConfiguration.swift b/Example/Configurations/HapticFeedbackConfiguration.swift deleted file mode 100644 index 180b4d4..0000000 --- a/Example/Configurations/HapticFeedbackConfiguration.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// HapticFeedbackConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The HapticFeedbackConfiguration -class HapticFeedbackConfiguration: Configuration { - - /// The Title - let title: String = "HaptifFeedback 📳" - - /// The Subtitle - let subtitle: String = "Enable or Disable the HapticFeedback" - - /// The Options - let options = [ - "Disabled", - "Enabled" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - configuration.completionButton.hapticFeedback = nil - configuration.detailButton?.hapticFeedback = nil - } else { - configuration.completionButton.hapticFeedback = .impact(.medium) - configuration.detailButton?.hapticFeedback = .impact(.medium) - } - } - -} diff --git a/Example/Configurations/ItemsCountConfiguration.swift b/Example/Configurations/ItemsCountConfiguration.swift deleted file mode 100644 index 4e4bba3..0000000 --- a/Example/Configurations/ItemsCountConfiguration.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ItemsCountConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 10.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The ItemsCountConfiguration -class ItemsCountConfiguration: Configuration { - - /// The Title - let title: String = "Number of Items 📏" - - /// The Subtitle - let subtitle: String = "Set the amount of visible items" - - /// The Options - let options = [ - "4", - "3", - "2", - "1", - "Many" - ] - - /// The selected Index - var selectedIndex: Int = 0 { - didSet { - guard self.options.indices.contains(self.selectedIndex) else { - return - } - let selectedOption = self.options[self.selectedIndex] - if let numberOfVisibleItems = Int(selectedOption) { - ExampleViewController.numberOfVisibleItems = numberOfVisibleItems - } else if selectedOption == "Many" { - ExampleViewController.numberOfVisibleItems = 5 - } - } - } - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) {} - -} diff --git a/Example/Configurations/LayoutConfiguration.swift b/Example/Configurations/LayoutConfiguration.swift deleted file mode 100644 index c065902..0000000 --- a/Example/Configurations/LayoutConfiguration.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// LayoutConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The LayoutConfiguration -class LayoutConfiguration: Configuration { - - /// The Title - let title: String = "Layout 📐" - - /// The Subtitle - let subtitle: String = "Define the Layout" - - /// The Options - let options = [ - "Left", - "Centered", - "Right" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - configuration.itemsView.layout = .left - } else if self.selectedIndex == 1 { - configuration.itemsView.layout = .centered - } else if self.selectedIndex == 2 { - configuration.itemsView.layout = .right - } - } - -} diff --git a/Example/Configurations/SecondaryTitleColorConfiguration.swift b/Example/Configurations/SecondaryTitleColorConfiguration.swift deleted file mode 100644 index 2f00bd2..0000000 --- a/Example/Configurations/SecondaryTitleColorConfiguration.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SecondaryTitleColorConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 14.12.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The SecondaryTitleColorConfiguration -class SecondaryTitleColorConfiguration: Configuration { - - /// The Title - let title: String = "Secondary Title Color 🖍" - - /// The Subtitle - let subtitle: String = "Enable or Disable the secondary title color" - - /// The Options - let options = [ - "Disabled", - "Enabled" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - configuration.titleView.secondaryColor = nil - } else { - configuration.titleView.secondaryColor = .init( - startIndex: 0, - length: 5, - color: configuration.tintColor - ) - } - } - -} diff --git a/Example/Configurations/TintColorConfiguration.swift b/Example/Configurations/TintColorConfiguration.swift deleted file mode 100644 index 32de3b4..0000000 --- a/Example/Configurations/TintColorConfiguration.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// TintColorConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import WhatsNewKit - -/// The TintColorConfiguration -class TintColorConfiguration: Configuration { - - /// The Title - let title: String = "TintColor 🖌" - - /// The Subtitle - let subtitle: String = "Pick a tint color" - - /// The Options - let options = [ - "Blue", - "LightBlue", - "Orange", - "Purple", - "Red", - "Green" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - let tintColor: UIColor - switch self.selectedIndex { - case 0: - tintColor = .whatsNewKitBlue - case 1: - tintColor = .whatsNewKitLightBlue - case 2: - tintColor = .orange - case 3: - tintColor = .whatsNewKitPurple - case 4: - tintColor = .whatsNewKitRed - case 5: - tintColor = .whatsNewKitGreen - default: - return - } - configuration.detailButton?.titleColor = tintColor - configuration.completionButton.backgroundColor = tintColor - } - -} diff --git a/Example/Configurations/TitleModeConfiguration.swift b/Example/Configurations/TitleModeConfiguration.swift deleted file mode 100644 index 22a7f82..0000000 --- a/Example/Configurations/TitleModeConfiguration.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// TitleModeConfiguration.swift -// WhatsNewKit-Example -// -// Created by Timothy Ellis on 11/4/20. -// Copyright © 2020 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The TitleModeConfiguration -class TitleModeConfiguration: Configuration { - - /// The Title - let title: String = "Title Mode 🔧" - - /// The Subtitle - let subtitle: String = "Define how the title will be shown" - - /// The Options - let options = [ - "Fixed", - "Scrolls" - ] - - /// The selected Index - var selectedIndex: Int = 0 - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - configuration.titleView.titleMode = .fixed - } else if self.selectedIndex == 1 { - configuration.titleView.titleMode = .scrolls - } - } - -} diff --git a/Example/Configurations/VersionStoreConfiguration.swift b/Example/Configurations/VersionStoreConfiguration.swift deleted file mode 100644 index a28c7c8..0000000 --- a/Example/Configurations/VersionStoreConfiguration.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// VersionStoreConfiguration.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import Foundation -import WhatsNewKit - -/// The VersionStoreConfiguration -class VersionStoreConfiguration: Configuration { - - /// The Title - let title: String = "VersionStore 💾" - - /// The Subtitle - let subtitle: String = "Enable or Disable the WhatsNewVersionStore" - - /// The Options - let options = [ - "Disabled", - "Enabled" - ] - - /// The selected Index - var selectedIndex: Int = 0 { - didSet { - if self.selectedIndex == 0 { - ExampleViewController.versionStore = nil - } - } - } - - /// Configure WhatsNewViewController.Configuration - /// - /// - Parameter configuration: The WhatsNewViewController.Configuration - func configure(configuration: inout WhatsNewViewController.Configuration) { - if self.selectedIndex == 0 { - ExampleViewController.versionStore = nil - } else if ExampleViewController.versionStore == nil { - ExampleViewController.versionStore = InMemoryWhatsNewVersionStore() - } - } - -} diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..85cfb1d --- /dev/null +++ b/Example/Example.xcodeproj/project.pbxproj @@ -0,0 +1,494 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 3D5FCF782767887F00D3211F /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5FCF772767887F00D3211F /* App.swift */; }; + 3D5FCF7A2767887F00D3211F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5FCF792767887F00D3211F /* ContentView.swift */; }; + 3D5FCF88276788FE00D3211F /* WhatsNewKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D5FCF87276788FE00D3211F /* WhatsNewKit */; }; + 3D9996C8276E112A00438FB6 /* ExamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA482052769E7F900F526B0 /* ExamplesView.swift */; }; + 3D9996C9276E112C00438FB6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5FCF792767887F00D3211F /* ContentView.swift */; }; + 3D9996CB276E113100438FB6 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5FCF772767887F00D3211F /* App.swift */; }; + 3D9996CD276E118C00438FB6 /* WhatsNewKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D9996CC276E118C00438FB6 /* WhatsNewKit */; }; + 3D999702276E141A00438FB6 /* Assets-macOS.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D999701276E141A00438FB6 /* Assets-macOS.xcassets */; }; + 3D999705276E144B00438FB6 /* Assets-iOS.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D999703276E144800438FB6 /* Assets-iOS.xcassets */; }; + 3DA482062769E7F900F526B0 /* ExamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA482052769E7F900F526B0 /* ExamplesView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3D5FCF742767887F00D3211F /* WhatsNewKit-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WhatsNewKit-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D5FCF772767887F00D3211F /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 3D5FCF792767887F00D3211F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3D5FCF85276788F900D3211F /* WhatsNewKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WhatsNewKit; path = ..; sourceTree = ""; }; + 3D9996B8276E10CE00438FB6 /* Example-macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D9996C3276E10D000438FB6 /* Entitlements.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.entitlements; sourceTree = ""; }; + 3D999701276E141A00438FB6 /* Assets-macOS.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Assets-macOS.xcassets"; sourceTree = ""; }; + 3D999703276E144800438FB6 /* Assets-iOS.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Assets-iOS.xcassets"; sourceTree = ""; }; + 3DA482052769E7F900F526B0 /* ExamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3D5FCF712767887E00D3211F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5FCF88276788FE00D3211F /* WhatsNewKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D9996B5276E10CE00438FB6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D9996CD276E118C00438FB6 /* WhatsNewKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3D5FCF6B2767887E00D3211F = { + isa = PBXGroup; + children = ( + 3D5FCF762767887F00D3211F /* Example */, + 3D5FCF752767887F00D3211F /* Products */, + 3D5FCF86276788FE00D3211F /* Frameworks */, + 3D5FCF85276788F900D3211F /* WhatsNewKit */, + ); + sourceTree = ""; + }; + 3D5FCF752767887F00D3211F /* Products */ = { + isa = PBXGroup; + children = ( + 3D5FCF742767887F00D3211F /* WhatsNewKit-Example.app */, + 3D9996B8276E10CE00438FB6 /* Example-macOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 3D5FCF762767887F00D3211F /* Example */ = { + isa = PBXGroup; + children = ( + 3D5FCF772767887F00D3211F /* App.swift */, + 3D5FCF792767887F00D3211F /* ContentView.swift */, + 3DA482052769E7F900F526B0 /* ExamplesView.swift */, + 3D999700276E131F00438FB6 /* Resources */, + ); + path = Example; + sourceTree = ""; + }; + 3D5FCF86276788FE00D3211F /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 3D999700276E131F00438FB6 /* Resources */ = { + isa = PBXGroup; + children = ( + 3D999703276E144800438FB6 /* Assets-iOS.xcassets */, + 3D999701276E141A00438FB6 /* Assets-macOS.xcassets */, + 3D9996C3276E10D000438FB6 /* Entitlements.entitlements */, + ); + path = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3D5FCF732767887E00D3211F /* Example-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D5FCF822767888000D3211F /* Build configuration list for PBXNativeTarget "Example-iOS" */; + buildPhases = ( + 3D5FCF702767887E00D3211F /* Sources */, + 3D5FCF712767887E00D3211F /* Frameworks */, + 3D5FCF722767887E00D3211F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Example-iOS"; + packageProductDependencies = ( + 3D5FCF87276788FE00D3211F /* WhatsNewKit */, + ); + productName = Example; + productReference = 3D5FCF742767887F00D3211F /* WhatsNewKit-Example.app */; + productType = "com.apple.product-type.application"; + }; + 3D9996B7276E10CE00438FB6 /* Example-macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D9996C4276E10D000438FB6 /* Build configuration list for PBXNativeTarget "Example-macOS" */; + buildPhases = ( + 3D9996B4276E10CE00438FB6 /* Sources */, + 3D9996B5276E10CE00438FB6 /* Frameworks */, + 3D9996B6276E10CE00438FB6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Example-macOS"; + packageProductDependencies = ( + 3D9996CC276E118C00438FB6 /* WhatsNewKit */, + ); + productName = "Example-macOS"; + productReference = 3D9996B8276E10CE00438FB6 /* Example-macOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3D5FCF6C2767887E00D3211F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1310; + TargetAttributes = { + 3D5FCF732767887E00D3211F = { + CreatedOnToolsVersion = 13.1; + }; + 3D9996B7276E10CE00438FB6 = { + CreatedOnToolsVersion = 13.2; + }; + }; + }; + buildConfigurationList = 3D5FCF6F2767887E00D3211F /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3D5FCF6B2767887E00D3211F; + productRefGroup = 3D5FCF752767887F00D3211F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3D5FCF732767887E00D3211F /* Example-iOS */, + 3D9996B7276E10CE00438FB6 /* Example-macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3D5FCF722767887E00D3211F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D999705276E144B00438FB6 /* Assets-iOS.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D9996B6276E10CE00438FB6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D999702276E141A00438FB6 /* Assets-macOS.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3D5FCF702767887E00D3211F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5FCF7A2767887F00D3211F /* ContentView.swift in Sources */, + 3DA482062769E7F900F526B0 /* ExamplesView.swift in Sources */, + 3D5FCF782767887F00D3211F /* App.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D9996B4276E10CE00438FB6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D9996C9276E112C00438FB6 /* ContentView.swift in Sources */, + 3D9996CB276E113100438FB6 /* App.swift in Sources */, + 3D9996C8276E112A00438FB6 /* ExamplesView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3D5FCF802767888000D3211F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 3D5FCF812767888000D3211F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3D5FCF832767888000D3211F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-iOS"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.Example-iOS"; + PRODUCT_NAME = "WhatsNewKit-Example"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3D5FCF842767888000D3211F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-iOS"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.Example-iOS"; + PRODUCT_NAME = "WhatsNewKit-Example"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3D9996C5276E10D000438FB6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-macOS"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Example/Resources/Entitlements.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.Example-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 3D9996C6276E10D000438FB6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-macOS"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Example/Resources/Entitlements.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.Example-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3D5FCF6F2767887E00D3211F /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D5FCF802767888000D3211F /* Debug */, + 3D5FCF812767888000D3211F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D5FCF822767888000D3211F /* Build configuration list for PBXNativeTarget "Example-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D5FCF832767888000D3211F /* Debug */, + 3D5FCF842767888000D3211F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D9996C4276E10D000438FB6 /* Build configuration list for PBXNativeTarget "Example-macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D9996C5276E10D000438FB6 /* Debug */, + 3D9996C6276E10D000438FB6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3D5FCF87276788FE00D3211F /* WhatsNewKit */ = { + isa = XCSwiftPackageProductDependency; + productName = WhatsNewKit; + }; + 3D9996CC276E118C00438FB6 /* WhatsNewKit */ = { + isa = XCSwiftPackageProductDependency; + productName = WhatsNewKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 3D5FCF6C2767887E00D3211F /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/WhatsNewKit.xcodeproj/xcshareddata/xcschemes/WhatsNewKit-iOS.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme similarity index 52% rename from WhatsNewKit.xcodeproj/xcshareddata/xcschemes/WhatsNewKit-iOS.xcscheme rename to Example/Example.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme index 4b80304..da2bdc1 100644 --- a/WhatsNewKit.xcodeproj/xcshareddata/xcschemes/WhatsNewKit-iOS.xcscheme +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme @@ -1,6 +1,6 @@ + BlueprintIdentifier = "3D5FCF732767887E00D3211F" + BuildableName = "WhatsNewKit-Example.app" + BlueprintName = "Example-iOS" + ReferencedContainer = "container:Example.xcodeproj"> @@ -26,28 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - + + BlueprintIdentifier = "3D5FCF732767887E00D3211F" + BuildableName = "WhatsNewKit-Example.app" + BlueprintName = "Example-iOS" + ReferencedContainer = "container:Example.xcodeproj"> - + - + + BlueprintIdentifier = "3D5FCF732767887E00D3211F" + BuildableName = "WhatsNewKit-Example.app" + BlueprintName = "Example-iOS" + ReferencedContainer = "container:Example.xcodeproj"> - + diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example-macOS.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example-macOS.xcscheme new file mode 100644 index 0000000..b98877e --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example-macOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example/App.swift b/Example/Example/App.swift new file mode 100644 index 0000000..55e8f7b --- /dev/null +++ b/Example/Example/App.swift @@ -0,0 +1,93 @@ +import SwiftUI +import WhatsNewKit + +// MARK: - App + +/// The App +@main +struct App {} + +// MARK: - SwiftUI.App + +extension App: SwiftUI.App { + + /// The content and behavior of the app. + var body: some Scene { + WindowGroup { + ContentView() + .environment( + \.whatsNew, + .init( + versionStore: InMemoryWhatsNewVersionStore(), + whatsNewCollection: self + ) + ) + } + } + +} + +// MARK: - App+WhatsNewCollectionProvider + +extension App: WhatsNewCollectionProvider { + + /// A WhatsNewCollection + var whatsNewCollection: WhatsNewCollection { + WhatsNew( + version: "1.0.0", + title: "WhatsNewKit", + features: [ + .init( + image: .init( + systemName: "star.fill", + foregroundColor: .orange + ), + title: "Showcase your new App Features", + subtitle: "Present your new app features just like a native app from Apple." + ), + .init( + image: .init( + systemName: "wand.and.stars", + foregroundColor: .cyan + ), + title: "Automatic Presentation", + subtitle: .init( + try! AttributedString( + markdown: "Simply declare a WhatsNew per Version and present it automatically by using the `.whatsNewSheet()` modifier." + ) + ) + ), + .init( + image: .init( + systemName: "gear.circle.fill", + foregroundColor: .gray + ), + title: "Configuration", + subtitle: "Easily adjust colors, strings, haptic feedback, behaviours and the layout of the presented WhatsNewView to your needs." + ), + .init( + image: .init( + systemName: "swift", + foregroundColor: .init(.init(red: 240.0 / 255, green: 81.0 / 255, blue: 56.0 / 255, alpha: 1)) + ), + title: "Swift Package Manager", + subtitle: "WhatsNewKit can be easily integrated via the Swift Package Manager." + ) + ], + primaryAction: .init( + hapticFeedback: { + #if os(iOS) + .notification(.success) + #else + nil + #endif + }() + ), + secondaryAction: .init( + title: "Learn more", + action: .openURL(.init(string: "https://github.com/SvenTiigi/WhatsNewKit")) + ) + ) + } + +} diff --git a/Example/Example/ContentView.swift b/Example/Example/ContentView.swift new file mode 100644 index 0000000..80bc9dc --- /dev/null +++ b/Example/Example/ContentView.swift @@ -0,0 +1,24 @@ +import SwiftUI +import WhatsNewKit + +// MARK: - ContentView + +/// The ContentView +struct ContentView {} + +// MARK: - View + +extension ContentView: View { + + /// The content and behavior of the view + var body: some View { + NavigationView { + ExamplesView() + } + #if !os(macOS) + .navigationViewStyle(.stack) + #endif + .whatsNewSheet() + } + +} diff --git a/Example/Example/ExamplesView.swift b/Example/Example/ExamplesView.swift new file mode 100644 index 0000000..60483db --- /dev/null +++ b/Example/Example/ExamplesView.swift @@ -0,0 +1,238 @@ +import SwiftUI +import WhatsNewKit + +// MARK: - ExamplesView + +/// The ExamplesView +struct ExamplesView { + + /// The Examples + private let examples = WhatsNew.Example.allCases + + /// The currently presented WhatsNew object + @State + private var whatsNew: WhatsNew? + +} + +// MARK: - View + +extension ExamplesView: View { + + /// The content and behavior of the view + var body: some View { + List { + Section( + header: Text( + verbatim: "Examples" + ), + footer: Text( + verbatim: "Tap on an example to manually present a WhatsNewView" + ) + ) { + ForEach( + self.examples, + id: \.rawValue + ) { example in + Button( + action: { + self.whatsNew = example.whatsNew + } + ) { + Text( + verbatim: example.displayName + ) + } + } + } + } + .navigationTitle("WhatsNewKit") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Link( + destination: .init( + string: "https://github.com/SvenTiigi/WhatsNewKit" + )! + ) { + Image(systemName: "text.book.closed.fill") + } + } + } + .sheet( + whatsNew: self.$whatsNew + ) + } + +} + +// MARK: - WhatsNew+Example + +private extension WhatsNew { + + /// A WhatsNew Example + enum Example: String, Codable, Hashable, CaseIterable { + /// Calendar + case calendar + /// Maps + case maps + /// Translate + case translate + } + +} + +// MARK: - WhatsNew+Example+displayName + +private extension WhatsNew.Example { + + /// The user friendly display name + var displayName: String { + self.rawValue.prefix(1).capitalized + self.rawValue.dropFirst() + } + +} + +// MARK: - WhatsNew+Example+whatsNew + +private extension WhatsNew.Example { + + /// The WhatsNew + var whatsNew: WhatsNew { + switch self { + case .calendar: + return .init( + title: "What's New in Calendar", + features: [ + .init( + image: .init( + systemName: "envelope", + foregroundColor: .red + ), + title: "Found Events", + subtitle: "Siri suggests events found in Mail, Messages, and Safari, so you can add them easily, such as flight reservations and hotel bookings." + ), + .init( + image: .init( + systemName: "clock", + foregroundColor: .red + ), + title: "Time to Leave", + subtitle: "Calendar uses Apple Maps to look up locations, traffic conditions, and transit options to tell you when it's time to leave." + ), + .init( + image: .init( + systemName: "location", + foregroundColor: .red + ), + title: "Location Suggestions", + subtitle: "Calendar suggests locations based on your past events and significant locations." + ) + ], + primaryAction: .init( + backgroundColor: .red + ) + ) + case .maps: + return .init( + title: "What's New in Maps", + features: [ + .init( + image: .init( + systemName: "map.fill", + foregroundColor: .green + ), + title: "Updated Map Style", + subtitle: "An improved design makes it easier to navigate and explore the map." + ), + .init( + image: .init( + systemName: "mappin.and.ellipse", + foregroundColor: .pink + ), + title: "All-New Place Cards", + subtitle: "Completely redesigned place cards make it easier to learn about and interact with places." + ), + .init( + image: .init( + systemName: "magnifyingglass", + foregroundColor: .blue + ), + title: "Improved Search", + subtitle: "Finding places is now easier with filters and automatic updates when you're browsing results on the map." + ) + ], + primaryAction: .init(backgroundColor: .blue), + secondaryAction: .init( + title: "About Apple Maps & Privacy", + foregroundColor: .blue, + action: .openURL(.init(string: "maps://")) + ) + ) + case .translate: + return .init( + title: .init( + text: .init( + "What's New in " + + AttributedString( + "Translate", + attributes: .foregroundColor(.cyan) + ) + ) + ), + features: [ + .init( + image: .init( + systemName: "rectangle.portrait.bottomthird.inset.filled", + foregroundColor: .cyan + ), + title: "Conversation Views", + subtitle: "Choose a side-by-side or face-to-face conversation view." + ), + .init( + image: .init( + systemName: "mic", + foregroundColor: .cyan + ), + title: "Auto Translate", + subtitle: "Respond in conversations without tapping the microphone button." + ), + .init( + image: .init( + systemName: "iphone", + foregroundColor: .cyan + ), + title: "System-Wide Translation", + subtitle: "Translate selected text anywhere on your iPhone." + ) + ], + primaryAction: .init( + backgroundColor: .cyan + ), + secondaryAction: .init( + title: "About Translation & Privacy", + foregroundColor: .cyan, + action: .openURL( + .init(string: "https://apple.com/privacy") + ) + ) + ) + } + } + +} + +// MARK: - AttributeContainer+foregroundColor + +private extension AttributeContainer { + + /// A AttributeContainer with a given foreground color + /// - Parameter color: The foreground color + static func foregroundColor( + _ color: Color + ) -> Self { + var container = Self() + container.foregroundColor = color + return container + } + +} diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AccentColor.colorset/Contents.json b/Example/Example/Resources/Assets-iOS.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..6112869 --- /dev/null +++ b/Example/Example/Resources/Assets-iOS.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-1024px.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-1024px.png new file mode 100644 index 0000000..6b5ac9e Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-1024px.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-40pt@3x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-40pt@3x.png new file mode 100644 index 0000000..864402c Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-40pt@3x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-60pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-60pt@2x.png new file mode 100644 index 0000000..864402c Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-120px-60pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-152px-76pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-152px-76pt@2x.png new file mode 100644 index 0000000..6531a4f Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-152px-76pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-167px-83.5pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-167px-83.5pt@2x.png new file mode 100644 index 0000000..c1b6b08 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-167px-83.5pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-180px-60pt@3x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-180px-60pt@3x.png new file mode 100644 index 0000000..fad5d5f Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-180px-60pt@3x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-20px-20pt@1x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-20px-20pt@1x.png new file mode 100644 index 0000000..079bac1 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-20px-20pt@1x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-29px-29pt@1x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-29px-29pt@1x.png new file mode 100644 index 0000000..db6c2b7 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-29px-29pt@1x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-20pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-20pt@2x.png new file mode 100644 index 0000000..127e765 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-20pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-40pt@1x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-40pt@1x.png new file mode 100644 index 0000000..127e765 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-40px-40pt@1x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-58px-29pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-58px-29pt@2x.png new file mode 100644 index 0000000..425373a Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-58px-29pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-60px-20pt@3x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-60px-20pt@3x.png new file mode 100644 index 0000000..423558f Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-60px-20pt@3x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-76px-76pt@1x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-76px-76pt@1x.png new file mode 100644 index 0000000..50b8992 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-76px-76pt@1x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-80px-40pt@2x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-80px-40pt@2x.png new file mode 100644 index 0000000..9e5056d Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-80px-40pt@2x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-87px-29pt@3x.png b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-87px-29pt@3x.png new file mode 100644 index 0000000..6fdd336 Binary files /dev/null and b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/AppIcon-87px-29pt@3x.png differ diff --git a/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/Contents.json b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/Contents.json new file mode 100644 index 0000000..4ac3ec4 --- /dev/null +++ b/Example/Example/Resources/Assets-iOS.xcassets/AppIcon-iOS.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "AppIcon-40px-20pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-60px-20pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-58px-29pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-87px-29pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-80px-40pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-120px-40pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-120px-60pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-180px-60pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-20px-20pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-40px-20pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29px-29pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-58px-29pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40px-40pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-80px-40pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-76px-76pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-152px-76pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-167px-83.5pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "AppIcon-1024px.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Assets-iOS.xcassets/Contents.json b/Example/Example/Resources/Assets-iOS.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Example/Resources/Assets-iOS.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AccentColor.colorset/Contents.json b/Example/Example/Resources/Assets-macOS.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..6112869 --- /dev/null +++ b/Example/Example/Resources/Assets-macOS.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/Contents.json b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/Contents.json new file mode 100644 index 0000000..f93a293 --- /dev/null +++ b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "macOS-AppIcon-16px-16pt@1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "macOS-AppIcon-32px-16pt@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "macOS-AppIcon-32px-32pt@1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "macOS-AppIcon-64px-32pt@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "macOS-AppIcon-128px-128pt@1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "macOS-AppIcon-256px-128pt@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "macOS-AppIcon-256px-256pt@1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "macOS-AppIcon-512px-256pt@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "macOS-AppIcon-512px.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "macOS-AppIcon-1024px.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-1024px.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-1024px.png new file mode 100644 index 0000000..04395d5 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-1024px.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-128px-128pt@1x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-128px-128pt@1x.png new file mode 100644 index 0000000..df049a0 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-128px-128pt@1x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-16px-16pt@1x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-16px-16pt@1x.png new file mode 100644 index 0000000..adb6dc9 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-16px-16pt@1x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-128pt@2x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-128pt@2x.png new file mode 100644 index 0000000..52c3314 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-128pt@2x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-256pt@1x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-256pt@1x.png new file mode 100644 index 0000000..52c3314 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-256px-256pt@1x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-16pt@2x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-16pt@2x.png new file mode 100644 index 0000000..71577b7 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-16pt@2x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-32pt@1x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-32pt@1x.png new file mode 100644 index 0000000..71577b7 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-32px-32pt@1x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px-256pt@2x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px-256pt@2x.png new file mode 100644 index 0000000..8a211e2 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px-256pt@2x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px.png new file mode 100644 index 0000000..8a211e2 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-512px.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-64px-32pt@2x.png b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-64px-32pt@2x.png new file mode 100644 index 0000000..cbcc2c8 Binary files /dev/null and b/Example/Example/Resources/Assets-macOS.xcassets/AppIcon-macOS.appiconset/macOS-AppIcon-64px-32pt@2x.png differ diff --git a/Example/Example/Resources/Assets-macOS.xcassets/Contents.json b/Example/Example/Resources/Assets-macOS.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Example/Resources/Assets-macOS.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Resources/Entitlements.entitlements b/Example/Example/Resources/Entitlements.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Example/Example/Resources/Entitlements.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Example/ExampleViewController/ConfigurationCollectionViewCell.swift b/Example/ExampleViewController/ConfigurationCollectionViewCell.swift deleted file mode 100644 index abfab05..0000000 --- a/Example/ExampleViewController/ConfigurationCollectionViewCell.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// ConfigurationCollectionViewCell.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import WhatsNewKit - -/// The ConfigurationCollectionViewCell -class ConfigurationCollectionViewCell: UICollectionViewCell, ReuseIdentifiable { - - // MARK: Properties - - /// The TitleLabel - lazy var titleLabel: UILabel = { - let label = UILabel() - label.textColor = .white - label.textAlignment = .center - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.font = .systemFont(ofSize: 16) - return label - }() - - /// Boolean if is selected - override var isSelected: Bool { - didSet { - UIView.animate( - withDuration: 0.5, - delay: 0, - usingSpringWithDamping: 0.9, - initialSpringVelocity: 0, - options: .curveLinear, - animations: { [weak self] in - if self?.isSelected == true { - self?.backgroundColor = self?.preferredBackgroundColor - self?.transform = CGAffineTransform( - scaleX: 1.09, - y: 1.07 - ) - } else { - self?.transform = .identity - self?.backgroundColor = .mainGray - } - } - ) - } - } - - /// The preferred BackgroundColor - var preferredBackgroundColor: UIColor { - switch self.titleLabel.text { - case "Blue": - return .main - case "LightBlue": - return .whatsNewKitLightBlue - case "Orange": - return .orange - case "Purple": - return .whatsNewKitPurple - case "Red": - return .whatsNewKitRed - case "Green": - return .whatsNewKitGreen - default: - return .main - } - } - - // MARK: Initializer - - /// Designated Initializer - /// - /// - Parameter frame: The Frame - override init(frame: CGRect) { - super.init(frame: frame) - // Add TitleLabel - self.contentView.addSubview(self.titleLabel) - // Add Corner Radius - self.layer.cornerRadius = 10 - // Set default BackgroundColor - self.backgroundColor = .mainGray - } - - /// Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// Layout Subviews - override func layoutSubviews() { - super.layoutSubviews() - self.titleLabel.frame = self.contentView.bounds - } - -} diff --git a/Example/ExampleViewController/ConfigurationsTableViewCell.swift b/Example/ExampleViewController/ConfigurationsTableViewCell.swift deleted file mode 100644 index f2b979e..0000000 --- a/Example/ExampleViewController/ConfigurationsTableViewCell.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// ConfigurationsTableViewCell.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - ConfigurationsTableViewCellDelegate - -/// The ConfigurationsTableViewCellDelegate -protocol ConfigurationsTableViewCellDelegate: class { - - /// Did select Option - /// - /// - Parameter option: The selected option string - func didSelect(option: String?) - -} - -// MARK: - ConfigurationsTableViewCell - -/// The ConfigurationsTableViewCell -class ConfigurationsTableViewCell: UITableViewCell, ReuseIdentifiable { - - // MARK: Properties - - /// The Configuration - var configuration: Configuration - - /// The Delegate - weak var delegate: ConfigurationsTableViewCellDelegate? - - /// The CollectionView - lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.itemSize = CGSize(width: 80, height: 80) - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.contentInset = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15) - collectionView.dataSource = self - collectionView.delegate = self - collectionView.showsVerticalScrollIndicator = false - collectionView.showsHorizontalScrollIndicator = false - collectionView.backgroundColor = .white - collectionView.register( - ConfigurationCollectionViewCell.self, - forCellWithReuseIdentifier: ConfigurationCollectionViewCell.reuseIdentifier - ) - return collectionView - }() - - // MARK: Initializer - - /// Designated Initializer with Configuration - /// - /// - Parameter configuration: The Configuration - init(configuration: Configuration) { - // Set Configuration - self.configuration = configuration - // Super init - super.init( - style: .default, - reuseIdentifier: ConfigurationCollectionViewCell.reuseIdentifier - ) - // Add CollectionView - self.contentView.addSubview(self.collectionView) - // Pre-Select the first Item - self.collectionView.selectItem( - at: IndexPath(item: 0, section: 0), - animated: false, - scrollPosition: .left - ) - } - - /// Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// Layout SubViews - override func layoutSubviews() { - super.layoutSubviews() - self.collectionView.frame = self.contentView.bounds - } - -} - -// MARK: - UICollectionViewDataSource - -extension ConfigurationsTableViewCell: UICollectionViewDataSource { - - func numberOfSections( - in collectionView: UICollectionView - ) -> Int { - // There is only one section - return 1 - } - - func collectionView( - _ collectionView: UICollectionView, - numberOfItemsInSection section: Int - ) -> Int { - // Return the options count - return self.configuration.options.count - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - // Dequeue Cell - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: ConfigurationCollectionViewCell.reuseIdentifier, - for: indexPath - ) - // Check if Cell is a ConfigurationCollectionViewCell - if let cell = cell as? ConfigurationCollectionViewCell { - // Set Option Text - cell.titleLabel.text = self.configuration.options[indexPath.item] - } - // Return Cell - return cell - } - -} - -// MARK: - UICollectionViewDelegate - -extension ConfigurationsTableViewCell: UICollectionViewDelegate { - - func collectionView( - _ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath - ) { - // Update the selected Index - self.configuration.selectedIndex = indexPath.item - // Invoke delegate - self.delegate?.didSelect(option: self.configuration.options[safe: indexPath.item]) - } - -} diff --git a/Example/ExampleViewController/ExampleViewController+PresentWhatsNew.swift b/Example/ExampleViewController/ExampleViewController+PresentWhatsNew.swift deleted file mode 100644 index 2a34345..0000000 --- a/Example/ExampleViewController/ExampleViewController+PresentWhatsNew.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// ExampleViewController+PresentWhatsNew.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import WhatsNewKit - -extension ExampleViewController { - - /// Present the WhatsNewViewController - func presentWhatsNewViewController() { - - // MARK: Step 1: Initialize the WhatsNew.Items - - // Easy Setup Item - let easySetupItem = WhatsNew.Item( - title: "Easy Setup", - subtitle: "The simple and typesafe WhatsNew struct enables you to structurize your awesome new app features", - image: #imageLiteral(resourceName: "setup") - ) - // Themes Item - let themesItem = WhatsNew.Item( - title: "Themes", - subtitle: "You can apply different themes to perfectly match with your existing app design", - image: #imageLiteral(resourceName: "themes") - ) - // Installation Item - let installationItem = WhatsNew.Item( - title: "Installation", - subtitle: "You can install WhatsNewKit via CocoaPods and Carthage", - image: #imageLiteral(resourceName: "installation") - ) - // Open Source Item - let openSourceItem = WhatsNew.Item( - title: "Open Source", - subtitle: "Contributions are\nvery welcome 👨‍💻", - image: #imageLiteral(resourceName: "openSource") - ) - - // Initialize Items - var items = [ - easySetupItem, - themesItem, - installationItem, - openSourceItem - ] - - // Check if number of visible items exceeds the maximum - if ExampleViewController.numberOfVisibleItems > ExampleViewController.maximumNumberOfVisibleItems { - for _ in 0...2 { - items.append(contentsOf: items) - } - } else { - items = .init( - items.dropLast(ExampleViewController.maximumNumberOfVisibleItems - ExampleViewController.numberOfVisibleItems) - ) - } - - // MARK: Step 2: Initialize WhatsNew with title and items - - // Initialize WhatsNew - let whatsNew = WhatsNew( - title: "WhatsNewKit", - items: items - ) - - // MARK: Step 3: Initialize a WhatsNewViewController Configuration - - // Initialize WhatsNewViewController Configuration in order to customize the Layout and behaviour - var configuration = WhatsNewViewController.Configuration( - theme: .default, - detailButton: .init( - // Detail Button Title - title: "Read more", - // Detail Button Action - action: .website(url: "https://github.com/SvenTiigi/WhatsNewKit") - ), - completionButton: .init( - // Completion Button Title - title: "Continue", - // Completion Button Action - action: .dismiss - ) - ) - - // Example-Application specific in order to apply configurations that has been selected - self.configurations.forEach { - $0.configure(configuration: &configuration) - } - - // MARK: Step 3: Initialize and present a WhatsNewViewController - - // Declare WhatsNewViewController - let whatsNewViewController: WhatsNewViewController? - - // Check if a WhatsNewVersionStore is available/enabled - if let versionStore = ExampleViewController.versionStore { - // Initialize WhatsNewViewController with WhatsNewVersionStore - whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew, - configuration: configuration, - versionStore: versionStore - ) - } else { - // Initialize WhatsNewViewController - whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew, - configuration: configuration - ) - } - - /// Check if WhatsNewViewController is available - if let controller = whatsNewViewController { - // Present the WhatsNewViewController - self.present(controller, animated: true) - } else { - // Present already presented Alert - self.presentAlreadyPresentedAlert() - } - } - -} diff --git a/Example/ExampleViewController/ExampleViewController.swift b/Example/ExampleViewController/ExampleViewController.swift deleted file mode 100644 index 1cc3857..0000000 --- a/Example/ExampleViewController/ExampleViewController.swift +++ /dev/null @@ -1,312 +0,0 @@ -// -// ExampleViewController.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import SafariServices -import WhatsNewKit - -// MARK: - ExampleViewController - -/// The ExampleViewController -class ExampleViewController: UIViewController { - - // MARK: Properties - - /// The static WhatsNewVersionStore - static var versionStore: WhatsNewVersionStore? - - /// The number of visible Items - static var numberOfVisibleItems: Int = 4 - - /// The maximum number of visible Items - static let maximumNumberOfVisibleItems: Int = 4 - - /// The HeaderView - lazy var headerView: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.addSubview(self.demoImageView) - view.addSubview(self.textLabel) - view.addSubview(self.dividerLine) - view.frame.size.height = 420 - return view - }() - - /// The DemoImageView - lazy var demoImageView: UIImageView = { - let imageView = UIImageView(image: .whitePreview) - imageView.contentMode = .scaleAspectFit - return imageView - }() - - /// The TextLabel - lazy var textLabel: UILabel = { - let label = UILabel() - label.text = "WhatsNewKit enables you to easily showcase your awesome new app features" - label.font = .systemFont(ofSize: 16, weight: .medium) - label.textColor = .whatsNewKitDark - label.textAlignment = .center - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - return label - }() - - /// The DividerLine - lazy var dividerLine: UIView = { - let view = UIView() - view.backgroundColor = .mainGray - return view - }() - - /// The TableView - lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .grouped) - tableView.tableHeaderView = self.headerView - tableView.delegate = self - tableView.dataSource = self - tableView.separatorStyle = .none - tableView.backgroundColor = .white - tableView.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: 50, - right: 0 - ) - return tableView - }() - - /// The Configurations - lazy var configurations: [Configuration] = [ - BackgroundColorConfiguration(), - TintColorConfiguration(), - AnimationConfiguration(), - VersionStoreConfiguration(), - LayoutConfiguration(), - ContentModeConfiguration(), - HapticFeedbackConfiguration(), - SecondaryTitleColorConfiguration(), - TitleModeConfiguration(), - ItemsCountConfiguration() - ] - - /// The Cells - lazy var cells: [UITableViewCell] = self.configurations.map { - let cell = ConfigurationsTableViewCell(configuration: $0) - cell.delegate = self - return cell - } - - // MARK: Initializer - - /// Designated Initializer - init() { - super.init(nibName: nil, bundle: nil) - } - - /// Required Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// ViewDidLoad - override func viewDidLoad() { - super.viewDidLoad() - // Set Title - self.title = "WhatsNewKit" - // Configure NavigationItem - self.configure(navigationItem: self.navigationItem) - // Configure Toolbar - self.configureToolbar() - } - - /// LoadView - override func loadView() { - // Replace View with TableView - self.view = self.tableView - } - - /// View did layout Subviews - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - self.demoImageView.frame = .init( - x: 0, - y: 25, - width: self.headerView.frame.width, - height: 300 - ) - self.textLabel.frame = .init( - x: 20, - y: self.demoImageView.frame.maxY + 10, - width: self.headerView.frame.width - 40, - height: self.headerView.frame.height - self.demoImageView.frame.maxY - 10 - 1 - ) - self.dividerLine.frame = .init( - x: 20, - y: self.textLabel.frame.maxY, - width: self.headerView.frame.width - 40, - height: 1 - ) - } - - // MARK: Customization - - /// Configure NavigationItem - /// - /// - Parameter navigationItem: The NavigationItem - func configure(navigationItem: UINavigationItem) { - // Initialize GitHub BarButtonItem - let gitHubBarButtonItem = UIBarButtonItem( - image: #imageLiteral(resourceName: "githubIconSmall"), - style: .plain, - target: self, - action: #selector(self.gitHubBarButtonItemTouched) - ) - navigationItem.rightBarButtonItem = gitHubBarButtonItem - } - - /// Confiugure Toolbar - func configureToolbar() { - // Show Toolbar - self.navigationController?.setToolbarHidden(false, animated: false) - // Initialize FlexibleSpace BarButtonItem - let flexibleSpaceBarButtonItem = UIBarButtonItem( - barButtonSystemItem: .flexibleSpace, - target: self, - action: nil - ) - // Initialize Present BarButtonItem - let presentBarButtonItem = UIBarButtonItem( - title: "Present 🤩", - style: .done , - target: self, - action: #selector(self.presentBarButtonItemTouched) - ) - presentBarButtonItem.tintColor = .main - // Set Toolbar Item - self.setToolbarItems( - [flexibleSpaceBarButtonItem, presentBarButtonItem, flexibleSpaceBarButtonItem], - animated: false - ) - } - - // MARK: BarButton Selectors - - /// Present GitHub ReadMe SafariViewController - @objc - private func gitHubBarButtonItemTouched(sender: UIBarButtonItem) { - // Verify URL can be constructed - guard let url = URL(string: "https://github.com/SvenTiigi/WhatsNewKit/blob/master/README.md") else { - print("Unable to construct GitHub Repo URL") - return - } - // Initialize SafariViewController with URL - let safariViewController = SFSafariViewController(url: url) - // Set tint color - safariViewController.preferredControlTintColor = .main - // Present SafariViewController - self.present(safariViewController, animated: true) - } - - @objc - func presentBarButtonItemTouched(sender: UIBarButtonItem) { - self.presentWhatsNewViewController() - } - -} - -// MARK: - UITableViewDataSource - -extension ExampleViewController: UITableViewDataSource { - - func numberOfSections( - in tableView: UITableView - ) -> Int { - return self.configurations.count - } - - func tableView( - _ tableView: UITableView, - numberOfRowsInSection section: Int - ) -> Int { - return 1 - } - - func tableView( - _ tableView: UITableView, - cellForRowAt indexPath: IndexPath - ) -> UITableViewCell { - self.cells[safe: indexPath.section] ?? .init(style: .default, reuseIdentifier: "invalidCell") - } - - func tableView( - _ tableView: UITableView, - titleForHeaderInSection section: Int - ) -> String? { - return self.configurations[safe: section]?.title - } - - func tableView( - _ tableView: UITableView, - titleForFooterInSection section: Int - ) -> String? { - return self.configurations[safe: section]?.subtitle - } - -} - -// MARK: - UITableViewDelegate - -extension ExampleViewController: UITableViewDelegate { - - func tableView( - _ tableView: UITableView, - heightForRowAt indexPath: IndexPath - ) -> CGFloat { - return 110 - } - -} - - -// MARK: - ConfigurationsTableViewCellDelegate - -extension ExampleViewController: ConfigurationsTableViewCellDelegate { - - /// Did select Option - /// - /// - Parameter option: The selected option string - func didSelect(option: String?) { - if option == "White" { - self.demoImageView.image = .whitePreview - } - if option == "Dark" { - self.demoImageView.image = .darkPreview - } - } - -} - -// MARK: - Already Presented - -extension ExampleViewController { - - /// Present already Presented Alert - func presentAlreadyPresentedAlert() { - let alertController = UIAlertController( - title: "WhatsNewVersionStore", - message: "The Version \(WhatsNew.Version.current().description) has already been presented", - preferredStyle: .alert - ) - let confirmAction = UIAlertAction(title: "Okay", style: .default, handler: nil) - alertController.addAction(confirmAction) - self.present(alertController, animated: true) - } - -} diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 212b38a..0000000 --- a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-Small-40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-Small-40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@3x.png", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-Small.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-Small@2x.png", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-167.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "iTunesArtwork@2x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "pre-rendered" : true - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png deleted file mode 100644 index 2c0e092..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png deleted file mode 100644 index b309c86..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png deleted file mode 100644 index c8e51b3..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png deleted file mode 100644 index 5a61bf7..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png deleted file mode 100644 index d04a2c1..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png deleted file mode 100644 index a6db5dc..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png deleted file mode 100644 index 9b9de3f..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png deleted file mode 100644 index 4bd1a32..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png deleted file mode 100644 index 5be3d43..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png deleted file mode 100644 index 4fdee0a..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png b/Example/Resources/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png deleted file mode 100644 index c5bb453..0000000 Binary files a/Example/Resources/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/Contents.json b/Example/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/Example/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/Preview/Contents.json b/Example/Resources/Assets.xcassets/Preview/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/Example/Resources/Assets.xcassets/Preview/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/Contents.json b/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/Contents.json deleted file mode 100644 index b4004a7..0000000 --- a/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "darkPreview.jpg", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/darkPreview.jpg b/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/darkPreview.jpg deleted file mode 100644 index c8f4c51..0000000 Binary files a/Example/Resources/Assets.xcassets/Preview/darkPreview.imageset/darkPreview.jpg and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/Contents.json b/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/Contents.json deleted file mode 100644 index 2e7200b..0000000 --- a/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "whitepreview.jpg", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/whitepreview.jpg b/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/whitepreview.jpg deleted file mode 100644 index ad31692..0000000 Binary files a/Example/Resources/Assets.xcassets/Preview/whitePreview.imageset/whitepreview.jpg and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/Contents.json b/Example/Resources/Assets.xcassets/WhatsNewItemImages/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/Example/Resources/Assets.xcassets/WhatsNewItemImages/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/Contents.json b/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/Contents.json deleted file mode 100644 index ab7b389..0000000 --- a/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icons8-puzzle-100.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/icons8-puzzle-100.png b/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/icons8-puzzle-100.png deleted file mode 100644 index 81aa7c1..0000000 Binary files a/Example/Resources/Assets.xcassets/WhatsNewItemImages/installation.imageset/icons8-puzzle-100.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/Contents.json b/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/Contents.json deleted file mode 100644 index 99c545b..0000000 --- a/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icons8-github-100.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/icons8-github-100.png b/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/icons8-github-100.png deleted file mode 100644 index f149402..0000000 Binary files a/Example/Resources/Assets.xcassets/WhatsNewItemImages/openSource.imageset/icons8-github-100.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/Contents.json b/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/Contents.json deleted file mode 100644 index 316996f..0000000 --- a/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icons8-approval-100.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/icons8-approval-100.png b/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/icons8-approval-100.png deleted file mode 100644 index f1a6c6d..0000000 Binary files a/Example/Resources/Assets.xcassets/WhatsNewItemImages/setup.imageset/icons8-approval-100.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/Contents.json b/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/Contents.json deleted file mode 100644 index 178484b..0000000 --- a/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icons8-picture-100.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/icons8-picture-100.png b/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/icons8-picture-100.png deleted file mode 100644 index 03b6e31..0000000 Binary files a/Example/Resources/Assets.xcassets/WhatsNewItemImages/themes.imageset/icons8-picture-100.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/Contents.json b/Example/Resources/Assets.xcassets/githubIconSmall.imageset/Contents.json deleted file mode 100644 index 1d3ecbb..0000000 --- a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icons8-github-filled-26.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icons8-github-filled-52.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icons8-github-filled-78.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-26.png b/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-26.png deleted file mode 100644 index 57c743d..0000000 Binary files a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-26.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-52.png b/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-52.png deleted file mode 100644 index 38816c5..0000000 Binary files a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-52.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-78.png b/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-78.png deleted file mode 100644 index 1b6741b..0000000 Binary files a/Example/Resources/Assets.xcassets/githubIconSmall.imageset/icons8-github-filled-78.png and /dev/null differ diff --git a/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/Contents.json b/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/Contents.json deleted file mode 100644 index 0723449..0000000 --- a/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "iTunesArtwork@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/iTunesArtwork@2x.png b/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/iTunesArtwork@2x.png deleted file mode 100644 index f84dcfd..0000000 Binary files a/Example/Resources/Assets.xcassets/roundedAppIcon.imageset/iTunesArtwork@2x.png and /dev/null differ diff --git a/Example/Resources/Base.lproj/LaunchScreen.storyboard b/Example/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index a2c4d71..0000000 --- a/Example/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Resources/Info.plist b/Example/Resources/Info.plist deleted file mode 100644 index 210b51b..0000000 --- a/Example/Resources/Info.plist +++ /dev/null @@ -1,48 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - WhatsNewKit - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.3.7 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortraitUpsideDown - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Light - - diff --git a/Example/Shared/Collection+Safe.swift b/Example/Shared/Collection+Safe.swift deleted file mode 100644 index 3d7e80a..0000000 --- a/Example/Shared/Collection+Safe.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Collection+Safe.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation - -extension Collection { - - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } - -} diff --git a/Example/Shared/ReuseIdentifiable.swift b/Example/Shared/ReuseIdentifiable.swift deleted file mode 100644 index 0d120b0..0000000 --- a/Example/Shared/ReuseIdentifiable.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ReuseIdentifiable.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation - -// MARK: - ReuseIdentifiable - -/// The ReuseIdentifiable -protocol ReuseIdentifiable { - - /// The reuse Identifier - static var reuseIdentifier: String { get } - -} - -// MARK: - ReuseIdentifiable Default Implementation - -extension ReuseIdentifiable { - - /// The reuse Identifier - static var reuseIdentifier: String { - // Return SubjectType of self as String - return String(describing: Mirror(reflecting: self).subjectType) - } - -} diff --git a/Example/Shared/UIColor+Main.swift b/Example/Shared/UIColor+Main.swift deleted file mode 100644 index aa764b1..0000000 --- a/Example/Shared/UIColor+Main.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// UIColor+Main.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 28.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit -import WhatsNewKit - -extension UIColor { - - // The main Color - static let main: UIColor = .whatsNewKitBlue - - /// The main gray color - static let mainGray: UIColor = .init(red: 199 / 255, green: 199 / 255, blue: 204 / 255, alpha: 1) - -} diff --git a/Example/Shared/UIImage+Assets.swift b/Example/Shared/UIImage+Assets.swift deleted file mode 100644 index da230eb..0000000 --- a/Example/Shared/UIImage+Assets.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UIImage+Assets.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 20.10.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -extension UIImage { - - /// The white preview image - static let whitePreview = UIImage(named: "whitePreview") - - /// The dark preview image - static let darkPreview = UIImage(named: "darkPreview") - -} diff --git a/LICENSE b/LICENSE index 3643f50..9a589c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Sven Tiigi +Copyright (c) 2022 Sven Tiigi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift index 6203e1f..fa03e7e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,29 +1,28 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.3 import PackageDescription let package = Package( name: "WhatsNewKit", platforms: [ - .iOS(.v9) + .iOS(.v13), + .macOS(.v11) ], products: [ .library( name: "WhatsNewKit", targets: ["WhatsNewKit"] - ), + ) ], - dependencies: [], targets: [ .target( name: "WhatsNewKit", - dependencies: [], path: "Sources" ), .testTarget( name: "WhatsNewKitTests", dependencies: ["WhatsNewKit"], path: "Tests" - ), + ) ] ) diff --git a/README.md b/README.md index c273780..b32aea2 100644 --- a/README.md +++ b/README.md @@ -1,675 +1,530 @@ +
+

- WhatsNewKit Header Logo + logo +

+ +

+ WhatsNewKit +

+ +

+ A Swift Package to easily showcase your new app features. +
+ It's designed from the ground up to be fully customized to your needs.

- - Swift 5.0 - CI Status - - Version - - - Carthage Compatible - -
- - Platform - Documentation + Platform Twitter

-
+Example -

- WhatsNewKit enables you to easily showcase your awesome new app features.
It's designed from the ground up to be fully customized to your needs. -

- -
+```swift +import SwiftUI +import WhatsNewKit -

- Demo -

+struct ContentView: View { + + var body: some View { + NavigationView { + // ... + } + .whatsNewSheet() + } + +} +``` ## Features -- [x] Customization and Configuration to your needs 💪 -- [x] Predefined Themes and Animations 🎬 -- [x] Easily check if your Features have already been presented 🎁 -- [x] Awesome UI 😍 - -## Example - -The example Application is an excellent way to see `WhatsNewKit` in action. You get a brief look of the available configuration options and how they affect the look and feel of the `WhatsNewViewController`. Simply open the `WhatsNewKit.xcodeproj` and run the `WhatsNewKit-Example` scheme. - -

-Example App -

+- [x] Easily present your new app features 🤩 +- [x] Automatic & Manual presentation mode ✅ +- [x] Support for SwiftUI, UIKit and AppKit 🧑‍🎨 +- [x] Runs on iOS and macOS 📱 🖥 +- [x] Adjustable layout 🔧 ## Installation -### CocoaPods - -WhatsNewKit is available through [CocoaPods](http://cocoapods.org). To install -it, simply add the following line to your Podfile: - -```bash -pod 'WhatsNewKit' -``` - -### Carthage - -[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - -To integrate WhatsNewKit into your Xcode project using Carthage, specify it in your `Cartfile`: - -```ogdl -github "SvenTiigi/WhatsNewKit" -``` - -Run `carthage update --platform iOS` to build the framework and drag the built `WhatsNewKit.framework` into your Xcode project. - -On your application targets’ “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase” and add the Framework path as mentioned in [Carthage Getting started Step 4, 5 and 6](https://github.com/Carthage/Carthage/blob/master/README.md#if-youre-building-for-ios-tvos-or-watchos) - ### Swift Package Manager To integrate using Apple's [Swift Package Manager](https://swift.org/package-manager/), add the following as a dependency to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/SvenTiigi/WhatsNewKit.git", from: "1.3.0") + .package(url: "https://github.com/SvenTiigi/WhatsNewKit.git", from: "2.0.0") ] ``` -### Manually +Or navigate to your Xcode project then select `Swift Packages`, click the “+” icon and search for `WhatsNewKit`. -If you prefer not to use any of the aforementioned dependency managers, you can integrate WhatsNewKit into your project manually. Simply drag the `Sources` Folder into your Xcode project. +## Example -## Usage -The following first usage description shows the easiest way of presenting your new app features with `WhatsNewKit`. +Check out the example application to see WhatsNewKit in action. Simply open the `Example/Example.xcodeproj` and run the "Example" scheme. -> 👨‍💻 Please see the [Advanced](https://github.com/SvenTiigi/WhatsNewKit#advanced) section for further configuration options and features. +

+ Example Applications +

-```swift -import WhatsNewKit +## Usage -// Initialize WhatsNew -let whatsNew = WhatsNew( - // The Title - title: "WhatsNewKit", - // The features you want to showcase - items: [ - WhatsNew.Item( - title: "Installation", - subtitle: "You can install WhatsNewKit via CocoaPods or Carthage", - image: UIImage(named: "installation") - ), - WhatsNew.Item( - title: "Open Source", - subtitle: "Contributions are very welcome 👨‍💻", - image: UIImage(named: "openSource") - ) - ] -) +### Table of contents -// Initialize WhatsNewViewController with WhatsNew -let whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew -) +- [Manual Presentation](https://github.com/SvenTiigi/WhatsNewKit/tree/master#manual-presentation) +- [Automatic Presentation](https://github.com/SvenTiigi/WhatsNewKit/tree/master#automatic-presentation) +- [WhatsNewEnvironment](https://github.com/SvenTiigi/WhatsNewKit/tree/master#whatsnewenvironment) +- [WhatsNewVersionStore](https://github.com/SvenTiigi/WhatsNewKit/tree/master#whatsnewversionstore) +- [WhatsNew](https://github.com/SvenTiigi/WhatsNewKit/tree/master#whatsnew) +- [Layout](https://github.com/SvenTiigi/WhatsNewKit/tree/master#layout) +- [WhatsNewViewController](https://github.com/SvenTiigi/WhatsNewKit/tree/master#whatsnewviewcontroller) -// Present it 🤩 -self.present(whatsNewViewController, animated: true) -``` +### Manual Presentation -Additionally, you can make use of the `WhatsNewVersionStore` to show the `WhatsNewViewController` only once for each version of your app. +If you wish to manually present a `WhatsNewView` you can make use of the `sheet(whatsNew:)` modifier. ```swift -// Initialize WhatsNewVersionStore -let versionStore: WhatsNewVersionStore = KeyValueWhatsNewVersionStore() - -// Passing a WhatsNewVersionStore to the initializer -// will give you an optional WhatsNewViewController -let whatsNewViewController: WhatsNewViewController? = WhatsNewViewController( - whatsNew: whatsNew, - versionStore: versionStore -) - -// Verify WhatsNewViewController is available -guard let viewController = whatsNewViewController else { - // The user has already seen the WhatsNew-Screen for the current Version of your app - return +struct ContentView: View { + + @State + var whatsNew: WhatsNew? = WhatsNew( + title: "WhatsNewKit", + features: [ + .init( + image: .init( + systemName: "star.fill", + foregroundColor: .orange + ), + title: "Showcase your new App Features", + subtitle: "Present your new app features..." + ), + // ... + ] + ) + + var body: some View { + NavigationView { + // ... + } + .sheet( + whatsNew: self.$whatsNew + ) + } + } - -// Present WhatsNewViewController -self.present(viewController, animated: true) ``` -> 👨‍💻 Head over to the [WhatsNewVersionStore](https://github.com/SvenTiigi/WhatsNewKit#whatsnewversionstore-) to learn more. -## Advanced -As mentioned before `WhatsNewKit` can be fully customized to your needs. The Advanced section will explain all configuration possibilities and features of `WhatsNewKit` in detail. First off it's important to understand the components of the `WhatsNewViewController` in order to customize the behaviour and `UI`-Design. +### Automatic Presentation -

-UILayout -

+The automatic presentation mode allows you to simply declare your new features via the SwiftUI Environment and WhatsNewKit will take care to present the corresponding `WhatsNewView`. -### WhatsNewViewController.Configuration -The [WhatsNewViewController.Configuration](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BConfiguration.swift) struct enables you to customize the `WhatsNewViewController` components to your needs. The configuration itself can be passed to the initializer of the `WhatsNewViewController`. +First add a `.whatsNewSheet()` modifier to the view where the `WhatsNewView` should be presented on. ```swift -// Initialize default Configuration -var configuration = WhatsNewViewController.Configuration() - -// Customize Configuration to your needs -configuration.backgroundColor = .white -configuration.titleView.titleColor = .orange -configuration.itemsView.titleFont = .systemFont(ofSize: 17) -configuration.detailButton?.titleColor = .orange -configuration.completionButton.backgroundColor = .orange -// And many more configuration properties... - -// Initialize WhatsNewViewController with custom configuration -let whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew, - configuration: configuration -) -``` - -### Theme 🌄 -A [Theme](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BConfiguration.swift) allows you to group the customization of a `WhatsNewViewController.Configuration`. `WhatsNewKit` implemented predefined Themes which are available as static properties both in white and dark mode. Or you create your very own Theme to configure it to your needs. - -| `.darkRed` | `.whiteRed` | -| ------------- | ------------- | -|

Theme Dark Red

|

Theme White Red

| +struct ContentView: View { -```swift -// Configuration with predefined Dark Red Theme -let darkRed = WhatsNewViewController.Configuration( - theme: .darkRed -) + var body: some View { + NavigationView { + // ... + } + // Automatically present a WhatsNewView, if needed. + // The WhatsNew that should be presented to the user + // is automatically retrieved from the `WhatsNewEnvironment` + .whatsNewSheet() + } -// Apply predefined White Red Theme to Configuration -var configuration = WhatsNewViewController.Configuration() -configuration.apply(theme: .whiteRed) - -// Or create your own Theme and initialize a Configuration with your Theme -let myTheme = WhatsNewViewController.Theme { configuration in - configuration.backgroundColor = .white - configuration.titleView.titleColor = .orange - configuration.itemsView.titleFont = .systemFont(ofSize: 17) - configuration.detailButton?.titleColor = .orange - configuration.completionButton.backgroundColor = .orange - // ... } - -// Initialize a Configuration with your Theme -let configuration = WhatsNewViewController.Configuration( - theme: myTheme -) ``` -For a full overview of the available predefined Themes check out the [Example-Application](https://github.com/SvenTiigi/WhatsNewKit/tree/master/Example). - -#### iOS 13 Dark-Mode - -Use the `.red` Theme if you wish that a predefined Theme like `.darkRed` and `.whiteRed` automatically adapts to the current [UserInterfaceStyle](https://developer.apple.com/documentation/uikit/uiuserinterfacestyle). +The `.whatsNewSheet()` modifier is making use of the `WhatsNewEnvironment` to retrieve an optional WhatsNew object that should be presented to the user for the current version. Therefore you can easily configure the `WhatsNewEnvironment` via the `environment` modifier. ```swift -// Configuration with predefine `red` Theme which auto adapts to the UserInterfaceStyle -// in order to support iOS 13 Dark-Mode -let configuration = WhatsNewViewController.Configuration( - theme: .red -) -``` - -Dark-Mode compatible Themes: `.blue`, `.lightBlue`, `.orange`, `.purple`, `.red`, `.green` - -### Layout 📐 +extension App: SwiftUI.App { + + var body: some Scene { + WindowGroup { + ContentView() + .environment( + \.whatsNew, + WhatsNewEnvironment( + // Specify in which way the presented WhatsNew Versions are stored. + // In default the `UserDefaultsWhatsNewVersionStore` is used. + versionStore: UserDefaultsWhatsNewVersionStore(), + // Pass a `WhatsNewCollectionProvider` or an array of WhatsNew instances + whatsNewCollection: self + ) + ) + } + } -`WhatsNewKit` comes with three predefined [`ItemsView.Layouts`](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BItemsView.swift). +} -| Left | Centered | Right | -| ------------- | ------------- | ------------- | -| Default Layout | Centered Layout | Right Layout | +// MARK: - App+WhatsNewCollectionProvider -```swift -// Left Layout -configuration.itemsView.layout = .left +extension App: WhatsNewCollectionProvider { -// Centered Layout -configuration.itemsView.layout = .centered + /// Declare your WhatsNew instances per version + var whatsNewCollection: WhatsNewCollection { + WhatsNew( + version: "1.0.0", + // ... + ) + WhatsNew( + version: "1.1.0", + // ... + ) + WhatsNew( + version: "1.2.0", + // ... + ) + } -// Right Layout -configuration.itemsView.layout = .right +} ``` -> ☝️ In default the ItemsView layout is set to `.left`. - -### ContentMode 📏 - -Setting the [`ContentMode`](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BItemsView.swift) in the `ItemsView` Configuration will adjust for how your features are arranged along the axis. - -| Top | Center | Fill | -| ------------- | ------------- | ------------- | -| ContentMode Top | ContentMode Center | ContentMode Fill - -```swift -// ContentMode Top -configuration.itemsView.contentMode = .top -// ContentMode Center -configuration.itemsView.contentMode = .center +## WhatsNewEnvironment -// ContentMode Fill -configuration.itemsView.contentMode = .fill -``` -> ☝️ In default the ItemsView ContentMode is set to `top`. - -### Insets +The `WhatsNewEnvironment` will take care to determine the matching WhatsNew object that should be presented to the user for the current version. -Additionally, if you wish you can modify the layout insets of the `WhatsNewViewController` components. +As seen in the previous example you can initialize a `WhatsNewEnvironment` by specifying the `WhatsNewVersionStore` and providing a `WhatsNewCollection`. ```swift -// Set TitleView Insets (Default values) -configuration.titleView.insets = UIEdgeInsets(top: 50, left: 20, bottom: 15, right: 20) +// Initialize WhatsNewEnvironment by passing an array of WhatsNew Instances. +// UserDefaultsWhatsNewVersionStore is used as default WhatsNewVersionStore +let whatsNewEnvironment = WhatsNewEnvironment( + whatsNewCollection: [ + WhatsNew( + version: "1.0.0", + // ... + ) + ] +) -// Increase the CompletionButton Bottom Inset -configuration.completionButton.insets.bottom += 10 +// Initialize WhatsNewEnvironment with NSUbiquitousKeyValueWhatsNewVersionStore +// which stores the presented versions in iCloud. +// WhatsNewCollection is provided by a `WhatsNewBuilder` closure +let whatsNewEnvironment = WhatsNewEnvironment( + versionStore: NSUbiquitousKeyValueWhatsNewVersionStore(), + whatsNewCollection: { + WhatsNew( + version: "1.0.0", + // ... + ) + } +) ``` -### ImageSize -In order to define the size of your images for each of your feature you can set an [ImageSize](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BItemsView.swift) on the `ItemsView` configuration. - -```swift -// Use the original image size as it is -configuration.itemsView.imageSize = .original - -// Use the preferred image size which fits perfectly :) -configuration.itemsView.imageSize = .preferred - -// Use a custom height for each image -configuration.itemsView.imageSize = .fixed(height: 25) -``` -> ☝️ In default the ItemsView ImageSize is set to `preferred`. +Additionally, the `WhatsNewEnvironment` includes a fallback for patch versions. For example when a user installs version `1.0.1` and you only have declared a `WhatsNew` for version `1.0.0` the environment will automatically fallback to version `1.0.0` and present the `WhatsNewView` to the user if needed. -### Image Tint-Color -In default WhatsNewKit auto tints the images of each `WhatsNew.Item` in the given tint color of the configuration. -If you wish to disable this functionality simply set `autoTintImage` to `false` +If you wish to further customize the behaviour of the `WhatsNewEnvironment` you can easily subclass it and override the `whatsNew()` function. ```swift -// Disable auto tinting images -configuration.itemsView.autoTintImage = false +class MyCustomWhatsNewEnvironment: WhatsNewEnvironment { + + /// Retrieve a WhatsNew that should be presented to the user, if available. + override func whatsNew() -> WhatsNew? { + // The current version + let currentVersion = self.currentVersion + // Your declared WhatsNew objects + let whatsNewCollection = self.whatsNewCollection + // The WhatsNewVersionStore used to determine the already presented versions + let versionStore = self.whatsNewVersionStore + // TODO: Determine WhatsNew that should be presented to the user... + } + +} ``` -> ☝️ In default the ItemsView AutoTintImage is set to `true`. -### Animation 🎬 -Animations +## WhatsNewVersionStore -You can apply a [Animation](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BAnimation.swift) to all components of the `WhatsNewViewController` via predefined animation types. In default all Animation properties are `nil` indicating no animation should be perfomed. +A `WhatsNewVersionStore` is a protocol type which is responsible for saving and retrieving versions that have been presented to the user. ```swift -// Set SlideUp Animation to TitleView -configuration.titleView.animation = .slideUp - -// Set SlideRight Animation to ItemsView -configuration.itemsView.animation = .slideRight - -// Set SlideLeft Animation to DetailButton -configuration.detailButton?.animation = .slideLeft +let whatsNewVersionStore: WhatsNewVersionStore -// Set SlideDown Animation to CompletionButton -configuration.completionButton.animation = .slideDown -``` +// Save presented versions +whatsNewVersionStore.save(presentedVersion: "1.0.0") -If you wish to animate all views with the same type you can do so by simply applying it to the configuration. +// Retrieve presented versions +let presentedVersions = whatsNewVersionStore.presentedVersions -```swift -// Global Animation-Type for all WhatsNewViewController components -configuration.apply(animation: .fade) +// Retrieve bool value if a given version has already been presented +let hasPresented = whatsNewVersionStore.hasPresented("1.0.0") ``` -If you wish to define your custom animation, simply set the `custom` enum and pass an animator closure. +WhatsNewKit comes along with three predefined implementations: ```swift -// Custom Animation for DetailButton -configuration.detailButton?.animation = .custom(animator: { (view: UIView, settings: AnimatorSettings) in - // view: The View to perform animation on - // settings: Preferred duration and delay -}) -``` - -### Title Mode -In default the TitleView is sticked to the top. -If you wish that the TitleView scrolls with the ItemsView you can change the `titleMode` on the TitleView configuration. - -```swift -// TitleView scrolls alongside with the ItemsView -configuration.titleView.titleMode = .scrolls - -// TitleView is fixed to top -configuration.titleView.titleMode = .fixed -``` -> ☝️ In default the titleMode is set to `.fixed`. +// Persists presented versions in the UserDefaults +let userDefaultsWhatsNewVersionStore = UserDefaultsWhatsNewVersionStore() -### Secondary Title Color -By setting a [SecondaryColor](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BTitleView.swift) on the TitleView you can change the color of certain characters. +// Persists presented versions in iCloud using the NSUbiquitousKeyValueStore +let ubiquitousKeyValueWhatsNewVersionStore = NSUbiquitousKeyValueWhatsNewVersionStore() -SecondaryColor - -```swift -// Set secondary color on TitleView Configuration -configuration.titleView.secondaryColor = .init( - // The start index - startIndex: 0, - // The length of characters - length: 5, - // The secondary color to apply - color: .whatsNewKitLightBlue -) +// Stores presented versions in memory. Perfect for testing purposes +let inMemoryWhatsNewVersionStore = InMemoryWhatsNewVersionStore() ``` -> ☝️ In default the secondaryColor is set to `nil`. -### DetailButton - DetailButton +If you already have a specific implementation to store user related settings like Realm or Core Data you can easily adopt your existing implementation to the `WhatsNewVersionStore` protocol. -By setting an [DetailButton](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BDetailButton.swift) struct on the `WhatsNewViewController.Configuration` struct you can customize the `title` and the corresponding `action` of the displayed detail button on the `WhatsNewViewController`. As the `DetailButton` struct is declared as optional the `WhatsNewViewController` will only display the button if a `DetailButton` configuration is available - -| Action | Description | -| ------------- | ------------- | -| `website` | When the user pressed the detail button a `SFSafariViewController` with the given `URL` will be presented | -| `custom` | After the detail button has been pressed by the user, your custom action will be invoked | - -```swift -// Initialize DetailButton with title and open website at url -let detailButton = WhatsNewViewController.DetailButton( - title: "Read more", - action: .website(url: "https://github.com/SvenTiigi/WhatsNewKit") -) +### NSUbiquitousKeyValueWhatsNewVersionStore -// Initialize DetailButton with title and custom action -let detailButton = WhatsNewViewController.DetailButton( - title: "Read more", - action: .custom(action: { [weak self] whatsNewViewController in - // Perform custom action on detail button pressed - }) -) -``` +If you are making use of the `NSUbiquitousKeyValueWhatsNewVersionStore` please ensure to enable the [iCloud Key-value storage](https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForKey-ValueDataIniCloud.html) capability in the "Signing & Capabilities" section of your Xcode project. -### CompletionButton - CompletionButton +

+ iCloud Key-value storage +

-The [CompletionButton](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BCompletionButton.swift) struct configures the displayed title and the action when the user pressed the completion button on the `WhatsNewViewController`. +## WhatsNew -| Action | Description | -| ------------- | ------------- | -| `dismiss` | When the user pressed the completion button, the `WhatsNewViewController` will be dismissed. This is the default value | -| `custom` | After the completion button has been pressed by the user, your custom action will be invoked | +The following sections explains how a `WhatsNew` struct can be initialized in order to describe the new features for a given version of your app. ```swift -// Initialize CompletionButton with title and dismiss action -let completionButton = WhatsNewViewController.CompletionButton( - title: "Continue", - action: .dismiss -) - -// Initialize CompletionButton with title and custom action -let completionButton = WhatsNewViewController.CompletionButton( - title: "Continue", - action: .custom(action: { [weak self] whatsNewViewController in - // Perform custom action on completion button pressed - }) +let whatsnew = WhatsNew( + // The Version that relates to the features you want to showcase + version: "1.0.0", + // The title that is shown at the top + title: "What's New", + // The features you want to showcase + features: [ + WhatsNew.Feature( + image: .init(systemName: "star.fill"), + title: "Title", + subtitle: "Subtitle" + ) + ], + // The primary action that is used to dismiss the WhatsNewView + primaryAction: WhatsNew.PrimaryAction( + title: "Continue", + backgroundColor: .accentColor, + foregroundColor: .white, + hapticFeedback: .notification(.success), + onDimiss: { + print("WhatsNewView has been dismissed") + } + ), + // The optional secondary action that is displayed above the primary action + secondaryAction: WhatsNew.SecondaryAction( + title: "Learn more", + foregroundColor: .accentColor, + hapticFeedback: .selection, + action: .openURL( + .init(string: "https://github.com/SvenTiigi/WhatsNewKit") + ) + ) ) ``` -### HapticFeedback 📳 +### WhatsNew.Version -You can enable on both `DetailButton` and `CompletionButton` haptic feedback when the user pressed one of these buttons. Either by setting the property or passing it to the initializer. +The `WhatsNew.Version` specifies the version that has introduced certain features to your app. ```swift -// Impact Feedback -button.hapticFeedback = .impact(.medium) - -// Selection Feedback -button.hapticFeedback = .selection - -// Notification Feedback with type -let completionButton = WhatsNewViewController.CompletionButton( - title: "Continue", - action: .dismiss, - hapticFeedback: .notification(.success) +// Initialize with major, minor, and patch +let version = WhatsNew.Version( + major: 1, + minor: 0, + patch: 0 ) -``` -> ☝️ In default the [HapticFeedback](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Configuration/WhatsNewViewController%2BHapticFeedback.swift) is `nil` indicating no haptic feedback should be executed. - -### iPad Adjustments -If you wish to modify the layout of the `WhatsNewViewController` when presenting it on an iPad you can set the `padAdjustment` closure. - -Currently the `padAdjustment` closure will only look for changed `insets` property of the `WhatsNewViewController.Configuration` in order to update the layout. Therefore, changes to any other configuration property will have no effect. +// Initialize by string literal +let version: WhatsNew.Version = "1.0.0" -```swift -// Set PadAdjustment closure -configuration.padAdjustment = { configuration in - // Invoke default PadAdjustments (Adjusts Insets for iPad) - WhatsNewViewController.Configuration.defaultPadAdjustment(&configuration) - // Adjust TitleView top insets - configuration.titleView.insets.top = 20 -} +// Initialize WhatsNew Version by using the current version of your bundle +let version: WhatsNew.Version = .current() ``` -> ☝️ In default the `WhatsNewViewController.Configuration.defaultPadAdjustment` will be invoked. -### WhatsNewVersionStore 💾 -

- WhatsNewVersionStore -

+### WhatsNew.Title -If we speak about presenting awesome new app features we have to take care that this kind of `UI` action only happens once if the user installed the app or opened it after an update. The `WhatsNewKit` offers a protocol oriented solution for this kind of problem via the [WhatsNewVersionStore](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Store/WhatsNewVersionStore.swift) protocol. +A `WhatsNew.Title` represents the title text that is rendered above the features. ```swift -/// WhatsNewVersionStore typealias protocol composition -public typealias WhatsNewVersionStore = WriteableWhatsNewVersionStore & ReadableWhatsNewVersionStore +// Initialize by string literla +let title: WhatsNew.Title = "Continue" -/// The WriteableWhatsNewVersionStore -public protocol WriteableWhatsNewVersionStore { - func set(version: WhatsNew.Version) -} +// Initialize with text and foreground color +let title = WhatsNew.Title( + text: "Continue", + foregroundColor: .primary +) -/// The ReadableWhatsNewVersionStore -public protocol ReadableWhatsNewVersionStore { - func has(version: WhatsNew.Version) -> Bool -} +// On >= iOS 15 initialize with AttributedString using Markdown +let title = WhatsNew.Title( + text: try AttributedString( + markdown: "What's **New**" + ) +) ``` -The `WhatsNewViewController` will use the APIs of the `WhatsNewVersionStore` in the following way. - -| API | Description | -| ------------- | ------------- | -| `has(version:)` | Checks if the `Whatsnew.Version` is available and will return `nil` during initialization. | -| `set(version:)` | The `WhatsNew.Version` will be set after the `CompletionButton` has been pressed. | +### WhatsNew.Feature -The `WhatsNewVersionStore` can be passed as an parameter to the initializer. If you do so the initializer will become `optional`. +A `WhatsNew.Feature` describe a specific feature of your app and generally consist of an image, title, and subtitle. ```swift -// Initialize WhatsNewViewController with WhatsNewVersionStore -let whatsNewViewController: WhatsNewViewController? = WhatsNewViewController( - whatsNew: whatsNew, - versionStore: myVersionStore +let feature = WhatsNew.Feature( + image: .init( + systemName: "wand.and.stars" + ), + title: "New Design", + subtitle: .init( + try AttributedString( + markdown: "An awesome new _Design_" + ) + ) ) - -// Check if WhatsNewViewController is available to present it. -if let controller = whatsNewViewController { - // Present it as WhatsNewViewController is available - // after init with WhatsNewVersionStore - self.present(controller, animated: true) -} else { - // WhatsNewViewController is `nil` this Version has already been presented -} - -// Or invoke present on the WhatsNewViewController -// to avoid the need of unwrapping the optional -whatsNewViewController?.present(on: self) ``` -> ☝️ Please keep in mind the `WhatsNewViewController` initializer will only become `optional` and checks if the Version has been already presented if you pass a `WhatsNewVersionStore` object. +### WhatsNew.PrimaryAction -#### Implementation -If you already handled saving user settings in your app to something like `Realm`, `CoreData` or `UserDefaults` you can conform that to the `WhatsNewVersionStore`. +The `WhatsNew.PrimaryAction` allows you to configure the behaviour of the primary button which is used to dismiss the presented `WhatsNewView` ```swift -// Extend your existing App-Logic -extension MyUserSettingsDatabase: WhatsNewVersionStore { - // Implement me 👨‍💻 -} +let primaryAction = WhatsNew.PrimaryAction( + title: "Continue", + backgroundColor: .blue, + foregroundColor: .white, + hapticFeedback: .notification(.success), + onDismiss: { + print("WhatsNewView has been dismissed") + } +) ``` +> Note: HapticFeedback will only be executed on iOS -#### Predefined Implementations - -`WhatsNewKit` brings along two predefined Implementations of the `WhatsNewVersionStore`. +### WhatsNew.SecondaryAction -##### KeyValueWhatsNewVersionStore - -The [KeyValueWhatsNewVersionStore](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Store/KeyValueWhatsNewVersionStore.swift) saves and retrieves the `WhatsNew.Version` via a `KeyValueable` protocol conform object. `UserDefaults` and `NSUbiquitousKeyValueStore` are already conform to that protocol 🙌 +A `WhatsNew.SecondaryAction` which is displayed above the `WhatsNew.PrimaryAction` can be optionally supplied when initializing a `WhatsNew` instance and allows you to present an additional View, perform a custom action or open an URL. ```swift -// Local KeyValueStore -let keyValueVersionStore = KeyValueWhatsNewVersionStore( - keyValueable: UserDefaults.standard +// SecondaryAction that presents a View +let secondaryActionPresentAboutView = WhatsNew.SecondaryAction( + title: "Learn more", + foregroundColor: .blue, + hapticFeedback: .selection, + action: .present { + AboutView() + } ) -// iCloud KeyValueStore -let keyValueVersionStore = KeyValueWhatsNewVersionStore( - keyValueable: NSUbiquitousKeyValueStore.default +// SecondaryAction that opens a URL +let secondaryActionOpenURL = WhatsNew.SecondaryAction( + title: "Read more", + foregroundColor: .blue, + hapticFeedback: .selection, + action: .open( + url: .init(string: "https://github.com/SvenTiigi/WhatsNewKit") + ) ) -// Initialize WhatsNewViewController with KeyValueWhatsNewVersionStore -let whatsNewViewController: WhatsNewViewController? = WhatsNewViewController( - whatsNew: whatsNew, - versionStore: keyValueVersionStore +// SecondaryAction with custom execution +let secondaryActionCustom = WhatsNew.SecondaryAction( + title: "Custom", + action: .custom { presentationMode in + // ... + } ) ``` +> Note: HapticFeedback will only be executed on iOS + +## Layout -##### InMemoryWhatsNewVersionStore +WhatsNewKit allows you to adjust the layout of a presented `WhatsNewView` in various ways. -The [InMemoryWhatsNewVersionStore](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Store/InMemoryWhatsNewVersionStore.swift) saves and retrieves the `WhatsNew.Version` in memory. Perfect for development or testing phase 👨‍💻 +The most simple way is by mutating the `WhatsNew.Layout.default` instance. ```swift -// Initialize WhatsNewViewController with InMemoryWhatsNewVersionStore -let whatsNewViewController: WhatsNewViewController? = WhatsNewViewController( - whatsNew: whatsNew, - versionStore: InMemoryWhatsNewVersionStore() -) +WhatsNew.Layout.default.featureListSpacing = 35 ``` -#### WhatsNew.Version -During the initialization of the `WhatsNew` struct the `WhatsNewKit` will automatically retrieve the current App-Version via the [CFBundleShortVersionString](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html) and construct a [WhatsNew.Version](https://github.com/SvenTiigi/WhatsNewKit/blob/master/Sources/Models/WhatsNew%2BVersion.swift) for you which is used by the `WhatsNewVersionStore` protocol in order to persist the presented app versions. If you want to manually set the version you can do it like the following example. +When using the automatic presentation style you can supply a default layout when initializing the WhatsNewEnvironment. ```swift -// Initialize Version 1.0.0 -let version = WhatsNew.Version( - major: 1, - minor: 0, - patch: 0 +.environment( + \.whatsNew, + .init( + defaultLayout: WhatsNew.Layout( + showsScrollViewIndicators: true, + featureListSpacing: 35 + ), + whatsNew: self + ) ) - -// Use a String literal -let version: WhatsNew.Version = "1.0.0" - -// Current Version in Bundle (Default) -let version = WhatsNew.Version.current() ``` -After you initialize a `WhatsNew.Version` you can pass it to the initializer of a `WhatsNew` struct. +Alternatively you can pass a `WhatsNew.Layout` when automatically or manually presenting the WhatsNewView ```swift -// Initialize WhatsNew with Title and Items -let whatsNew = WhatsNew( - version: version, - title: "WhatsNewKit", - items: [] +.whatsNewSheet( + layout: WhatsNew.Layout( + contentPadding: .init( + top: 80, + leading: 0, + bottom: 0, + trailing: 0 + ) + ) ) ``` -If you holding multiple `WhatsNew` structs in an array you can make use of the following two functions to retrieve a `WhatsNew` struct based on the `WhatsNewVersion`. - ```swift -let whatsNews: [WhatsNew] = [...] - -// Retrieve WhatsNew from array based on Version 1.0.0 -let whatsNewVersion1 = whatsNews.get(byVersion: - .init(major: 1, minor: 0, patch: 0) +.sheet( + whatsNew: self.$whatsNew, + layout: WhatsNew.Layout( + footerActionSpacing: 20 + ) ) +``` -// Or retrieve it via String as WhatsNew.Version is -// conform to the ExpressibleByStringLiteral protocol -let whatsNewVersion2 = whatsNews.get(byVersion: "2.0.0") +## WhatsNewViewController -// If you want the WhatsNew for your current App-Version -// based on the CFBundleShortVersionString from Bundle.main -let currentWhatsNew = whatsNews.get() +When using `UIKit` or `AppKit` you can make use of the `WhatsNewViewController`. +```swift +let whatsNewViewController = WhatsNewViewController( + whatsNew: WhatsNew( + version: "1.0.0", + // ... + ), + layout: WhatsNew.Layout( + contentSpacing: 80 + ) +) ``` -### Codable WhatsNew -The `WhatsNew` struct is conform the `Codable` protocol which allows you to initialize a `WhatsNew` struct via `JSON`. - -```JSON -{ - "version": { - "major": 1, - "minor": 0, - "patch": 0 - }, - "title": "WhatsNewKit", - "items": [ - { - "title": "Open Source", - "subtitle": "Contributions are very welcome 👨‍💻", - "image": "iVBORw0KGgoA..." - } - ] -} -``` -The optional `image` property of the `WhatsNew.Item` will be decoded and encoded in [Base64](https://en.wikipedia.org/wiki/Base64). +If you wish to present a `WhatsNewViewController` only if the version of the WhatsNew instance has not been presented you can make use of the convenience failable initializer. ```swift -// Encode to JSON -let encoded = try? JSONEncoder().encode(whatsNew) +// Verify WhatsNewViewController is available for presentation +guard let whatsNewViewController = WhatsNewViewController( + whatsNew: WhatsNew( + version: "1.0.0", + // ... + ), + versionStore: UserDefaultsWhatsNewVersionStore() +) else { + // Version of WhatsNew has already been presented + return +} -// Decode from JSON data -let decoded = try? JSONDecoder().decode(WhatsNew.self, from: data) +// Present WhatsNewViewController +// Version will be automatically saved in the provided +// WhatsNewVersionStore when the WhatsNewViewController gets dismissed +self.present(whatsNewViewController, animated: true) ``` -## Featured on - -* [Awesome iOS Weekly](http://weekly.awesomeios.com/issues/2#start) -* [Swift Weekly](http://digest.swiftweekly.com/issues/swift-weekly-issue-118-114740) -* [AppCoda Weekly](http://digest.appcoda.com/issues/appcoda-weekly-issue-74-116292) -* [iOS Goodies](http://ios-goodies.com/post/174437386181/week-232) -* [MyBridge - Open Source Swift Projects (June 2018)](https://medium.mybridge.co/swift-open-source-projects-of-the-month-v-june-2018-f9ce1239eee4) -* [The iOS Times](http://theiostimes.com/issue-44.html) -* [DZone](https://dzone.com/articles/this-week-in-mobile-11-june-2018) -* [Brian Advent](https://youtu.be/zCHEpN1Wgz4) -* [23 Amazing iOS UI Libraries written in Swift for the Past Year (v.2019)](https://medium.mybridge.co/23-amazing-ios-ui-libraries-written-in-swift-for-the-past-year-v-2019-3e5456318768) -* [Indie iOS Focus Weekly](https://indieiosfocus.com/issues/234) -* [5 iOS Libraries That Will Inspire Your Creativity](https://medium.com/better-programming/5-ios-libraries-that-will-inspire-your-creativity-26ee5837f9b7) - -## Contributing -Contributions are very welcome 🙌 🤓 - -## Credits -The `WhatsNew.Item` images ([icons8-github](https://icons8.com/icon/62856/github), [icons8-puzzle](https://icons8.com/icon/61018/puzzle), [icons8-approval](https://icons8.com/icon/59733/approval), [icons8-picture](https://icons8.com/icon/68826/picture)) which are seen on the screenshots and inside the example application are taken from [icons8.com](https://icons8.com/) which are licensed under [Creative Commons Attribution-NoDerivs 3.0 Unported](https://creativecommons.org/licenses/by-nd/3.0/). - ## License ``` WhatsNewKit -Copyright (c) 2020 Sven Tiigi +Copyright (c) 2022 Sven Tiigi sven.tiigi@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Sources/Collection/WhatsNewCollection.swift b/Sources/Collection/WhatsNewCollection.swift new file mode 100644 index 0000000..2333e8b --- /dev/null +++ b/Sources/Collection/WhatsNewCollection.swift @@ -0,0 +1,4 @@ +import Foundation + +/// A WhatsNewCollection type representing an array of WhatsNew elements +public typealias WhatsNewCollection = [WhatsNew] diff --git a/Sources/Collection/WhatsNewCollectionBuilder.swift b/Sources/Collection/WhatsNewCollectionBuilder.swift new file mode 100644 index 0000000..ceabecb --- /dev/null +++ b/Sources/Collection/WhatsNewCollectionBuilder.swift @@ -0,0 +1,53 @@ +import Foundation + +// MARK: - WhatsNewCollectionBuilder + +/// A WhatsNewCollectionBuilder +@resultBuilder +public enum WhatsNewCollectionBuilder {} + +// MARK: - ResultBuilder + +public extension WhatsNewCollectionBuilder { + + /// Build WhatsNewCollection + /// - Parameter components: The WhatsNew elements + static func buildBlock( + _ components: WhatsNewCollection.Element?... + ) -> WhatsNewCollection { + components.compactMap { $0 } + } + + /// Build optional WhatsNewCollection + /// - Parameter component: The optional WhatsNewCollection + static func buildOptional( + _ component: WhatsNewCollection? + ) -> WhatsNewCollection { + component ?? .init() + } + + /// Build either first WhatsNewCollection + /// - Parameter component: The first WhatsNewCollection + static func buildEither( + first component: WhatsNewCollection + ) -> WhatsNewCollection { + component + } + + /// Build either second WhatsNewCollection + /// - Parameter component: The second WhatsNewCollection + static func buildEither( + second component: WhatsNewCollection + ) -> WhatsNewCollection { + component + } + + /// Build array of WhatsNewCollections + /// - Parameter components: The array of WhatsNewCollections + static func buildArray( + _ components: [WhatsNewCollection] + ) -> WhatsNewCollection { + components.flatMap { $0 } + } + +} diff --git a/Sources/Collection/WhatsNewCollectionProvider.swift b/Sources/Collection/WhatsNewCollectionProvider.swift new file mode 100644 index 0000000..57aef57 --- /dev/null +++ b/Sources/Collection/WhatsNewCollectionProvider.swift @@ -0,0 +1,12 @@ +import Foundation + +// MARK: - WhatsNewProvider + +/// A WhatsNewCollection Provider type +public protocol WhatsNewCollectionProvider { + + /// A WhatsNewCollection + @WhatsNewCollectionBuilder + var whatsNewCollection: WhatsNewCollection { get } + +} diff --git a/Sources/Components/Buttons/WhatsNewButtonViewController+CompletionButton.swift b/Sources/Components/Buttons/WhatsNewButtonViewController+CompletionButton.swift deleted file mode 100644 index ce9cb64..0000000 --- a/Sources/Components/Buttons/WhatsNewButtonViewController+CompletionButton.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// WhatsNewButtonViewController+CompletionButton.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - CompletionButton - -extension WhatsNewButtonViewController { - - /// The CompletionButton - final class CompletionButton: UIButton { - - // MARK: Properties - - /// The highlighted background color - var highlightedBackgroundColor: UIColor = .white - - /// The onPress closure - var onPress: (() -> Void)? - - // MARK: Initializer - - /// Convenience initializer - /// - /// - Parameters: - /// - title: The title - /// - configuration: The Configuration - /// - onPress: The on press closure - convenience init( - title: String, - configuration: WhatsNewViewController.Configuration, - onPress: @escaping () -> Void - ) { - // Init with Custom Type - self.init(type: .custom) - // Set onPress closure - self.onPress = onPress - // Set highlighted background color - self.highlightedBackgroundColor = configuration.completionButton.backgroundColor - // Set title - self.setTitle(title, for: .normal) - // Perform configuration - self.configure(completionButton: configuration.completionButton) - } - - // MARK: View-Lifecycle - - /// Layout Subviews - override func layoutSubviews() { - super.layoutSubviews() - // Check if current background image is nil - if self.currentBackgroundImage == nil { - // Apply background color - self.applyBackgroundColor() - } - } - - /// TraitCollection did change - /// - Parameter previousTraitCollection: The previous TraitCollection - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - // Apply background color - self.applyBackgroundColor() - } - - } - -} - -// MARK: - Configure - -extension WhatsNewButtonViewController.CompletionButton { - - /// Configure with CompletionButton configuraton - /// - /// - Parameter completionButton: The CompletionButton Configuration - func configure( - completionButton: WhatsNewViewController.CompletionButton - ) { - // Set corner radius to rounded button - self.layer.cornerRadius = completionButton.cornerRadius - // Set mask to bound - self.layer.masksToBounds = true - // Set Content EdgeInsets - self.contentEdgeInsets = completionButton.contentEdgeInsets - // Set font - self.titleLabel?.font = completionButton.titleFont - // Set normal title color - self.setTitleColor(completionButton.titleColor, for: .normal) - // Set number of lines - self.titleLabel?.numberOfLines = 0 - // Set line break mode - self.titleLabel?.lineBreakMode = .byWordWrapping - // Add target - self.addTarget( - self, - action: #selector(self.didTouchUpInside), - for: .touchUpInside - ) - } - -} - -// MARK: - Apply Background Color - -extension WhatsNewButtonViewController.CompletionButton { - - /// Apply background color - func applyBackgroundColor() { - // Set the backgroundimage - self.setBackgroundImage( - .from( - self.highlightedBackgroundColor, - size: self.bounds.size - ), - for: .normal - ) - } - -} - -// MARK: - Target Handler - -extension WhatsNewButtonViewController.CompletionButton { - - /// Button did touch up inside - @objc - func didTouchUpInside() { - // Invoke onPress closure - self.onPress?() - } - -} - -// MARK: - UIImage+From - -private extension UIImage { - - /// Return a UIImage with a given UIColor and CGSize - /// - /// - Parameters: - /// - color: The color of the returned UIImage - /// - size: The size of the returned UIImage. Default value `width: 1, height: 1` - /// - Returns: Optional UIImage - static func from( - _ color: UIColor, - size: CGSize = .init(width: 1, height: 1) - ) -> UIImage? { - // Verify that the size is not equal to zero - guard size != .zero else { - // Otherwise return nil - return nil - } - // Initialize rect - let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - // Begin Graphics Context - UIGraphicsBeginImageContextWithOptions(size, false, 0) - // Set fill color - color.setFill() - // Fill rect with color - UIRectFill(rect) - // Retrieve Image from Graphics Context - let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext() - // End Graphics Context - UIGraphicsEndImageContext() - // Return image - return image - } - -} diff --git a/Sources/Components/Buttons/WhatsNewButtonViewController+DetailButton.swift b/Sources/Components/Buttons/WhatsNewButtonViewController+DetailButton.swift deleted file mode 100644 index e0fed3c..0000000 --- a/Sources/Components/Buttons/WhatsNewButtonViewController+DetailButton.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// WhatsNewButtonViewController+DetailButton.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - DetailButton - -extension WhatsNewButtonViewController { - - /// The DetailButton - final class DetailButton: UIButton { - - // MARK: Properties - - /// The onPress closure - let onPress: () -> Void - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - detailButton: The DetailButton configuration - /// - onPress: The on press closure - init( - detailButton: WhatsNewViewController.DetailButton?, - onPress: @escaping () -> Void - ) { - // Set on press - self.onPress = onPress - // Super init - super.init(frame: .zero) - // Configure DetailButton - self.configure(detailButton: detailButton) - } - - /// Initializer with Coder always returns nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - } - -} - -// MARK: - Configure - -extension WhatsNewButtonViewController.DetailButton { - - /// Configure with DetailButton Configuration - /// - /// - Parameter detailButton: The DetailButton Configuration - func configure( - detailButton: WhatsNewViewController.DetailButton? - ) { - // Set title - self.setTitle( - detailButton?.title, - for: .normal - ) - // Add target - self.addTarget( - self, - action: #selector(self.didTouchUpInside), - for: .touchUpInside - ) - // Set title color - self.setTitleColor( - detailButton?.titleColor, - for: .normal - ) - // Set font - self.titleLabel?.font = detailButton?.titleFont - } - -} - -// MARK: - Target Handler - -extension WhatsNewButtonViewController.DetailButton { - - /// Did touch up inside - @objc - func didTouchUpInside() { - // Invoke on press closure - self.onPress() - } - -} diff --git a/Sources/Components/Buttons/WhatsNewButtonViewController.swift b/Sources/Components/Buttons/WhatsNewButtonViewController.swift deleted file mode 100644 index 8bad324..0000000 --- a/Sources/Components/Buttons/WhatsNewButtonViewController.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// WhatsNewButtonViewController.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - WhatsNewButtonViewController - -/// The WhatsNewButtonViewController -final class WhatsNewButtonViewController: UIViewController { - - // MARK: Properties - - /// The Configuration - var configuration: WhatsNewViewController.Configuration - - /// The onPress closure - let onPress: (ButtonType) -> Void - - /// The completion Button - lazy var completionButton: UIButton = CompletionButton( - title: self.configuration.completionButton.title, - configuration: self.configuration, - onPress: { [weak self] in - // Invoke on press with completion button type - self?.onPress(.completion) - } - ) - - /// The detail button - lazy var detailButton: UIButton = DetailButton( - detailButton: self.configuration.detailButton, - onPress: { [weak self] in - // Invoke on press with detail button type - self?.onPress(.detail) - } - ) - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - configuration: The Configuration - /// - onPress: The onPress closure with ButtonType - init( - configuration: WhatsNewViewController.Configuration, - onPress: @escaping (ButtonType) -> Void - ) { - self.configuration = configuration - self.onPress = onPress - super.init(nibName: nil, bundle: nil) - // Hide View if an animation is available - self.view.isHidden = self.configuration.detailButton?.animation != nil - || self.configuration.completionButton.animation != nil - } - - /// Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// View did load - override func viewDidLoad() { - super.viewDidLoad() - // Set background color - self.view.backgroundColor = self.configuration.backgroundColor - // Add Subviews - self.addSubviews() - // Make Constraints - self.makeConstraints() - } - - /// View did appear - /// - /// - Parameter animated: If should be animated - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - // Disable isHidden - self.view.isHidden = false - // Perform animation if available - self.configuration.detailButton?.animation?.rawValue( - self.detailButton, - .init( - preferredDuration: 0.5, - preferredDelay: 0.3 - ) - ) - // Perform animation if available - self.configuration.completionButton.animation?.rawValue( - self.completionButton, - .init( - preferredDuration: 0.5, - preferredDelay: 0.4 - ) - ) - // Clear Animations - self.configuration.detailButton?.animation = nil - self.configuration.completionButton.animation = nil - } - -} - -// MARK: - Add Subviews - -extension WhatsNewButtonViewController { - - /// Add Subviews - func addSubviews() { - // Check if a DetailButton Configuration is available - if self.configuration.detailButton != nil { - // Add DetailButton to Subview - self.view.addSubview(self.detailButton) - } - // Add CompletionButton to Subview - self.view.addSubview(self.completionButton) - } - -} - -// MARK: - Make Constraints - -extension WhatsNewButtonViewController { - - /// Make Constraints - func makeConstraints() { - // Declare the CompletionButtonTopAnchor - let completionButtonTopAnchor: NSLayoutConstraint - // Check if a DetailButton Configuration is available - if let detailButton = self.configuration.detailButton { - // Make Constraints on DetailButton - self.detailButton.makeConstraints( - self.detailButton.topAnchor.constraint(equalTo: self.anchor.topAnchor), - self.detailButton.leadingAnchor.constraint(equalTo: self.anchor.leadingAnchor), - self.detailButton.trailingAnchor.constraint(equalTo: self.anchor.trailingAnchor) - ) - // Initialize TopAnchor - completionButtonTopAnchor = self.completionButton.topAnchor.constraint( - equalTo: self.detailButton.bottomAnchor, - constant: detailButton.bottomOffset - ) - } else { - // Initialize TopAnchor - completionButtonTopAnchor = self.completionButton.topAnchor.constraint( - equalTo: self.anchor.topAnchor - ) - } - // Make Constraints on CompletionButton - self.completionButton.makeConstraints( - completionButtonTopAnchor, - self.completionButton.leadingAnchor.constraint(equalTo: self.anchor.leadingAnchor), - self.completionButton.trailingAnchor.constraint(equalTo: self.anchor.trailingAnchor), - self.completionButton.bottomAnchor.constraint(equalTo: self.anchor.bottomAnchor) - ) - } - -} - -// MARK: - ButtonType - -extension WhatsNewButtonViewController { - - /// The ButtonType - enum ButtonType: String, Codable, Equatable, Hashable, CaseIterable { - /// Detail button - case detail - /// Completion button - case completion - } - -} diff --git a/Sources/Components/Items/WhatsNewItemsViewController+Cell.swift b/Sources/Components/Items/WhatsNewItemsViewController+Cell.swift deleted file mode 100644 index 258eb77..0000000 --- a/Sources/Components/Items/WhatsNewItemsViewController+Cell.swift +++ /dev/null @@ -1,295 +0,0 @@ -// -// WhatsNewItemsViewController+Cell.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Cell - -extension WhatsNewItemsViewController { - - /// The Cell - final class Cell: UITableViewCell { - - // MARK: Properties - - /// The Item - let item: WhatsNew.Item - - /// The Configuration - let configuration: WhatsNewViewController.Configuration - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - item: The WhatsNew Item - /// - configuration: The Configuration - init( - item: WhatsNew.Item, - configuration: WhatsNewViewController.Configuration - ) { - // Set item - self.item = item - // Set configuration - self.configuration = configuration - // Super init default style - super.init( - style: .default, - reuseIdentifier: String(describing: Cell.self) - ) - // Set background color - self.contentView.backgroundColor = self.configuration.backgroundColor - // Configure - self.configure() - } - - /// Initializer with Coder always returns nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// TraitCollection did change - /// - Parameter previousTraitCollection: The previous TraitCollection - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - // Re-Configure - self.configure() - } - - } - -} - -// MARK: - Configure - -extension WhatsNewItemsViewController.Cell { - - /// Perform configuration - func configure() { - // Check if ItemsView Layout is not eqaul to centered - if self.configuration.itemsView.layout != .centered { - // Perform ImageView Configuration - self.configureImageView() - } - // Perform TextLabel Configuration - self.configureTextLabel() - } - -} - -// MARK: - Configure TextLabel - -extension WhatsNewItemsViewController.Cell { - - /// Configure TextLabel - func configureTextLabel() { - // Set font - self.textLabel?.font = self.configuration.itemsView.subtitleFont - // Set text color - self.textLabel?.textColor = self.configuration.itemsView.subtitleColor - // Set number of lines to zero - self.textLabel?.numberOfLines = 0 - // Set line break mode to word wrapping - self.textLabel?.lineBreakMode = .byWordWrapping - // Set attributed text - self.textLabel?.attributedText = self.makeAttributedTextString() - // Set white background color - self.backgroundColor = .white - // Check if Layout is centered or right - if self.configuration.itemsView.layout == .centered { - // Set centered Text - self.textLabel?.textAlignment = .center - } else if self.configuration.itemsView.layout == .right { - // Set Text to right - self.textLabel?.textAlignment = .right - } - } - -} - -// MARK: - Configure ImageView - -extension WhatsNewItemsViewController.Cell { - - /// Configure ImageView - func configureImageView() { - // Enable scale aspect fit - self.imageView?.contentMode = .scaleAspectFit - // Clip to Bounds - self.imageView?.clipsToBounds = true - // Declare an optional UIImage - var image: UIImage? - // Switch on ImageSize - switch self.configuration.itemsView.imageSize { - case .original: - // Initialize Image with Item Image - image = self.item.image - case .fixed(let height): - // Initialize Image with resized Item Image - image = self.item.image?.resizeToSquare(ofSize: .init(height)) - } - // Check if autoTintImage is activated - if self.configuration.itemsView.autoTintImage { - // Tint image with tint color - image = image?.tint(color: self.configuration.tintColor) - } - // Set image - self.imageView?.image = image - } - -} - -// MARK: - Make AttributedString - -extension WhatsNewItemsViewController.Cell { - - /// Make AttributedString Text String - /// - /// - Returns: The Attributed String - func makeAttributedTextString() -> NSAttributedString { - // Declare Title Subtitle Attributed String - let titleSubtitleAttributedString: NSMutableAttributedString - // Check if title is empty - if self.item.title.isEmpty { - // Just return the item subtitle has no title is available - titleSubtitleAttributedString = .init(string: self.item.subtitle) - } else { - // Initialize attributed string - titleSubtitleAttributedString = .init(string: "\(self.item.title)\n\(self.item.subtitle)") - // Initialize Range - let range = NSRange(location: 0, length: self.item.title.utf16.count) - // Add title font - titleSubtitleAttributedString.addAttributes( - [.font: self.configuration.itemsView.titleFont], - range: range - ) - // Add title color - titleSubtitleAttributedString.addAttributes( - [.foregroundColor: self.configuration.itemsView.titleColor], - range: range - ) - } - // Check if Layout is centered - if self.configuration.itemsView.layout == .centered { - // Initialize a TextAttachment - let attachment = NSTextAttachment() - // Set Attachment Image - attachment.image = self.item.image - // Check if ItemsView should auto tint image - if self.configuration.itemsView.autoTintImage, - let tintedImage = attachment.image?.tint(color: self.configuration.tintColor) { - // Set tinted image - attachment.image = tintedImage - } - // Add Line Break at first position to TitleSubtitle String to create a spacing between Image and Text - titleSubtitleAttributedString.insert(.init(string: "\n\n"), at: 0) - // Initialize Attachment String - let attachmentString = NSAttributedString(attachment: attachment) - // Insert Attachment String at first position - titleSubtitleAttributedString.insert(attachmentString, at: 0) - } - // Return TitleSubtitle String - return titleSubtitleAttributedString - } - -} - -// MARK: - UIImage+resizeToSquare - -private extension UIImage { - - /// Resize UIImage to a square of the specified dimensions, preserving the aspect ratio. - /// - /// - Parameter ofSize: The size of the height and width of the destination square. - /// - Returns: The resized Image - func resizeToSquare(ofSize newSize: CGFloat) -> UIImage? { - // Verify the size is greater zero - guard newSize > 0 else { - // Otherwise return nil - return nil - } - // The returned image will be a square of the specified size - let containerSize = CGSize(width: newSize, height: newSize) - // The target size is the size of the image portion within the container. We preserve the aspect ratio, and ensure - // that it touches the container edges either on the left & right sides, or top & bottom (or both). - let targetSize: CGSize - if self.size.width > self.size.height { - targetSize = .init( - width: newSize, - height: (newSize / self.size.width) * self.size.height - ) - } else { - targetSize = .init( - width: (newSize / self.size.height) * self.size.width, - height: newSize - ) - } - // The origin for our redrawing, within the container, can be obtained by getting the difference between the container's - // dimensions and the target size dimensions. Divided by 2, - // since we want the redrawn image to be centered in the container. - let redrawOrigin = CGPoint( - x: (containerSize.width - targetSize.width) / 2, - y: (containerSize.height - targetSize.height) / 2 - ) - // Check if iOS 10 or greater is available - if #available(iOS 10.0, *) { - // Return rendererd Image with target size - return UIGraphicsImageRenderer(size: containerSize).image { [weak self] _ in - // Draw with target size - self?.draw(in: .init(origin: redrawOrigin, size: targetSize)) - } - } else { - // Begin Image Context with target size - UIGraphicsBeginImageContextWithOptions(containerSize, false, 0.0) - // Draw Image with target size - self.draw(in: .init(origin: redrawOrigin, size: targetSize)) - // Retrive Image from context - let image = UIGraphicsGetImageFromCurrentImageContext() - // End context - UIGraphicsEndImageContext() - // Return resized image - return image - } - } - -} - -// MARK: - UIImage+tint - -private extension UIImage { - - /// Tint Image with Color - /// - /// - Parameter color: The Color - /// - Returns: The tinted UIImage - func tint(color: UIColor) -> UIImage? { - // Verify that the size is not equal to zero - guard self.size != .zero else { - // Otherwise return untinted image - return self - } - // Retrieve image as template image - let image = self.withRenderingMode(.alwaysTemplate) - // Begin Image Context - UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) - // Set color - color.set() - // Draw image with size - image.draw(in: .init(origin: .zero, size: self.size)) - // Retrieve Image from context - let tintedImage = UIGraphicsGetImageFromCurrentImageContext() - // End context - UIGraphicsEndImageContext() - // Return tinted image - return tintedImage - } - -} diff --git a/Sources/Components/Items/WhatsNewItemsViewController.swift b/Sources/Components/Items/WhatsNewItemsViewController.swift deleted file mode 100644 index 2828758..0000000 --- a/Sources/Components/Items/WhatsNewItemsViewController.swift +++ /dev/null @@ -1,420 +0,0 @@ -// -// WhatsNewItemsViewController.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - WhatsNewItemsViewController - -/// The WhatsNewItemsViewController -final class WhatsNewItemsViewController: UIViewController { - - // MARK: Properties - - /// The WhatsNew Items - let items: [WhatsNew.Item] - - /// The Configuration - let configuration: WhatsNewViewController.Configuration - - /// The NotificationCenter - let notificationCenter: NotificationCenter - - /// The cell display count - var cellDisplayCount = 0 - - /// Bool if animation is disabled - var isAnimationDisabled: Bool = false - - /// The TableView - lazy var tableView: UITableView = { - // Initialize TableView - let tableView = UITableView(frame: .zero, style: .grouped) - // Set clear background color - tableView.backgroundColor = .clear - // Set data source - tableView.dataSource = self - // Set delegate - tableView.delegate = self - // No seperators - tableView.separatorStyle = .none - // No selection - tableView.allowsSelection = false - // Hide Vertical Scroll Indicator - tableView.showsVerticalScrollIndicator = false - // Set indicator style based on theme backgroundcolor - tableView.indicatorStyle = self.configuration.backgroundColor.isLight ? .black : .white - // Set estimatedRowHeight so autosizing will work on iOS 10 - tableView.estimatedRowHeight = 44.0 - // Set automtic dimension for row height - tableView.rowHeight = UITableView.automaticDimension - // Check if ItemsView Layout is right - if self.configuration.itemsView.layout == .right { - // Set semantic content attribute to force right to left - tableView.semanticContentAttribute = .forceRightToLeft - } - // Return TableView - return tableView - }() - - /// The Cells - lazy var cells: [Cell] = self.items.map { item in - .init( - item: item, - configuration: self.configuration - ) - } - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - items: The WhatsNew Items - /// - configuration: The Configuration - /// - notificationCenter: The NotificationCenter. Default value `default` - init( - items: [WhatsNew.Item], - configuration: WhatsNewViewController.Configuration, - notificationCenter: NotificationCenter = .default - ) { - // Set items - self.items = items - // Set configuration - self.configuration = configuration - // Set NotificationCenter - self.notificationCenter = notificationCenter - // Super init - super.init(nibName: nil, bundle: nil) - // Hide View if an animation is available - self.view.isHidden = self.configuration.itemsView.animation != nil - } - - /// Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - /// Deinit - deinit { - // Remove Obser - self.notificationCenter.removeObserver(self) - } - - // MARK: View-Lifecycle - - /// View did load - override func viewDidLoad() { - super.viewDidLoad() - // Set background color - self.view.backgroundColor = self.configuration.backgroundColor - // Add orientation did change observer - self.notificationCenter.addObserver( - self, - selector: #selector(self.orientationDidChange), - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - } - - /// Load View - override func loadView() { - self.view = self.tableView - } - - /// View did appear - /// - /// - Parameter animated: If should be animated - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - // Disable isHidden - self.view.isHidden = false - // Trigger reload data - self.tableView.reloadData() - } - - /// ScrollView will begin dragging - /// - /// - Parameter scrollView: The ScrollView - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - // Set is animation disabled to true to improve the user - // experience when having a large amount of Items - self.isAnimationDisabled = true - } - -} - -// MARK: - Notification Target - -extension WhatsNewItemsViewController { - - /// Orientation did change notification - /// - /// - Parameter notification: The Notification - @objc - func orientationDidChange(notification: Notification) { - // Check if the Cell Display count is not zero - if self.cellDisplayCount != 0 { - // Set is animation disabled to true - self.isAnimationDisabled = true - } - // Reload TableView - self.tableView.reloadData() - } - -} - -// MARK: - UITableViewDataSource - -extension WhatsNewItemsViewController: UITableViewDataSource { - - /// Retrieve number of sections - /// - /// - Parameter tableView: The TableView - /// - Returns: Amount of section - public func numberOfSections( - in tableView: UITableView - ) -> Int { - // Return items count - return self.items.count - } - - /// Retrieve number of rows in section - /// - /// - Parameters: - /// - tableView: The TableView - /// - section: The section - /// - Returns: The amount of rows in section - public func tableView( - _ tableView: UITableView, - numberOfRowsInSection section: Int - ) -> Int { - // Return one row - return 1 - } - - /// Retrieve cell for row at IndexPath - /// - /// - Parameters: - /// - tableView: The TableView - /// - indexPath: The IndexPath - /// - Returns: The configured Cell - public func tableView( - _ tableView: UITableView, - cellForRowAt indexPath: IndexPath - ) -> UITableViewCell { - // Initialize index - let index = indexPath.section - // Verify index is contained in indices - guard self.cells.indices.contains(index) else { - // Return unknown TableViewCell - return UITableViewCell( - style: .default, - reuseIdentifier: "unknown" - ) - } - // Return Cell - return self.cells[index] - } - -} - -// MARK: - UITableViewDelegate - -extension WhatsNewItemsViewController: UITableViewDelegate { - - /// TableView will display cell - /// - /// - Parameters: - /// - tableView: The TableView - /// - cell: The Cell - /// - indexPath: The indexPath - func tableView( - _ tableView: UITableView, - willDisplay cell: UITableViewCell, - forRowAt indexPath: IndexPath - ) { - // Set background color - cell.backgroundColor = self.configuration.backgroundColor - // Verify view is not hidden - guard !self.view.isHidden else { - // Otherwise return out of function - return - } - // Unwrap cell as Cell and verify cellDisplayCount is less then the items count - guard let cell = cell as? Cell, - !self.isAnimationDisabled, - self.cellDisplayCount < self.items.count else { - // Return out of function - return - } - // Increment CellDisplayCount - self.cellDisplayCount += 1 - // Animate Cell - self.configuration.itemsView.animation?.rawValue( - cell, - .init( - preferredDuration: 0.5, - preferredDelay: 0.15 * (Double(indexPath.section) + 1.0) - ) - ) - } - - /// Height for Header in section - /// - /// - Parameters: - /// - tableView: The TableView - /// - section: The Section - /// - Returns: The Header Height - func tableView( - _ tableView: UITableView, - heightForHeaderInSection section: Int - ) -> CGFloat { - return self.calculateSpace( - for: .header, - in: section, - contentMode: self.configuration.itemsView.contentMode - ) - } - - /// Height for Footer in section - /// - /// - Parameters: - /// - tableView: The TableView - /// - section: The Section - /// - Returns: The Footer Height - func tableView( - _ tableView: UITableView, - heightForFooterInSection section: Int - ) -> CGFloat { - return self.calculateSpace( - for: .footer, - in: section, - contentMode: self.configuration.itemsView.contentMode - ) - } - - /// View for Header in Section - /// - /// - Parameters: - /// - tableView: The TableView - /// - section: The Section - /// - Returns: The Header View - func tableView( - _ tableView: UITableView, - viewForHeaderInSection section: Int - ) -> UIView? { - return nil - } - - /// View for Footer in Section - /// - /// - Parameters: - /// - tableView: The TableView - /// - section: The Section - /// - Returns: The Footer View - func tableView( - _ tableView: UITableView, - viewForFooterInSection section: Int - ) -> UIView? { - return nil - } - -} - -// MARK: - Calculate Space - -extension WhatsNewItemsViewController { - - /// The Position - enum Position: String, Codable, Equatable, Hashable, CaseIterable { - /// Header - case header - /// Footer - case footer - } - - /// Calculate Space for Position in Section - /// - /// - Parameters: - /// - position: The Position - /// - section: The Section - /// - contentMode: The ContentMode - /// - Returns: The calculcated Space - func calculateSpace( - for position: Position, - in section: Int, - contentMode: WhatsNewViewController.ItemsView.ContentMode - ) -> CGFloat { - // Declare Divider - var divider: CGFloat - // Switch on ContentMode - switch contentMode { - case .top: - // Return zero Space - return 0 - case .center: - // Switch on position - switch position { - case .header: - // Verify Section is first - guard section == 0 else { - // Otherwise return zero - return 0 - } - case .footer: - // Verify Setion is last - guard section == self.numberOfSections(in: self.tableView) - 1 else { - // Otherwise return zero - return 0 - } - } - // Initialize Divider - divider = 2 - case .fill: - // Check if Items count is one - if self.items.count == 1 { - // Return calculated Space with Center ContentMode - return self.calculateSpace( - for: position, - in: section, - contentMode: .center - ) - } - // Switch on position - switch position { - case .header: - return 0 - case .footer: - // Verify Setion is not last - guard section != self.numberOfSections(in: self.tableView) - 1 else { - // Otherwise return zero - return 0 - } - } - // Initialize Divider - divider = .init(self.numberOfSections(in: self.tableView) - 1) - } - // Check if divider is zero - if divider == 0 { - // Re-Initializer Divider with one - divider = 1 - } - // Map Cells to their frame size and add them together - let height = self.cells.map { $0.frame.size.height }.reduce(0, +) - // Initialize Space - let space = (self.view.bounds.height - height) / divider - // Verify Space is not negative - guard space >= 10 else { - // Otherwise return zero - return 0 - } - // Return space - return space - } - -} diff --git a/Sources/Components/Title/WhatsNewTitleViewController.swift b/Sources/Components/Title/WhatsNewTitleViewController.swift deleted file mode 100644 index a9fcc24..0000000 --- a/Sources/Components/Title/WhatsNewTitleViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// WhatsNewTitleViewController.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - WhatsNewTitleViewController - -/// The WhatsNewTitleViewController -final class WhatsNewTitleViewController: UIViewController { - - // MARK: Properties - - /// The WhatsNew Title - let titleText: String - - /// The Configuration - var configuration: WhatsNewViewController.Configuration - - /// The title label - lazy var titleLabel: UILabel = { - let label = UILabel() - label.backgroundColor = .clear - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.textAlignment = self.configuration.titleView.titleAlignment - label.font = self.configuration.titleView.titleFont - label.textColor = self.configuration.titleView.titleColor - // Check if a secondary color is available - if let secondaryColor = self.configuration.titleView.secondaryColor { - // Set attributed text - label.attributedText = .init( - text: self.titleText, - colorConfiguration: secondaryColor - ) - } else { - // No secondary color available simply set text - label.text = self.titleText - } - return label - }() - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - title: The Title - /// - configuration: The Configuration - init( - title: String, - configuration: WhatsNewViewController.Configuration - ) { - // Set title - self.titleText = title - // Set configuration - self.configuration = configuration - // Super init - super.init(nibName: nil, bundle: nil) - // Set background color - self.view.backgroundColor = self.configuration.backgroundColor - // Hide View if an animation is available - self.view.isHidden = self.configuration.titleView.animation != nil - } - - /// Initializer with Coder always return nil - required init?(coder aDecoder: NSCoder) { - return nil - } - - // MARK: View-Lifecycle - - /// Load View - override func loadView() { - self.view = self.titleLabel - } - - /// View did appear - /// - /// - Parameter animated: If should be animated - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - // Disable isHidden - self.view.isHidden = false - // Perform animation if available - self.configuration.titleView.animation?.rawValue( - self.titleLabel, - .init( - preferredDuration: 0.5, - preferredDelay: 0.2 - ) - ) - // Clear Animation - self.configuration.titleView.animation = nil - } - -} - -// MARK: - NSAttributedString+Init - -private extension NSAttributedString { - - /// Convenience Initializer with Text and SecondaryColor Configuration - /// - /// - Parameters: - /// - text: The Text - /// - colorConfiguration: The SecondaryColor Configuration - convenience init( - text: String, - colorConfiguration: WhatsNewViewController.TitleView.SecondaryColor - ) { - // Initialize NSMutableAttributedString with text - let attributedString = NSMutableAttributedString(string: text) - // Check if start index and length matches the string - if text.dropFirst(colorConfiguration.startIndex).count >= colorConfiguration.length { - // Add foreground color attribute - attributedString.addAttributes( - [.foregroundColor: colorConfiguration.color], - range: .init( - location: colorConfiguration.startIndex, - length: colorConfiguration.length - ) - ) - } - // Init with AttributedString - self.init(attributedString: attributedString) - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+Animation.swift b/Sources/Configuration/WhatsNewViewController+Animation.swift deleted file mode 100644 index b395c8b..0000000 --- a/Sources/Configuration/WhatsNewViewController+Animation.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// WhatsNewViewController+Animation.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 06.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Animation - -public extension WhatsNewViewController { - - /// The Animation - enum Animation { - /// Fade - case fade - /// Slide up - case slideUp - /// Slide down - case slideDown - /// Slide left - case slideLeft - /// Slide right - case slideRight - /// Custom Animation - case custom(animator: Animator) - } - -} - -// MARK: - Animator Equatable - -extension WhatsNewViewController.Animation: Equatable { - - /// Returns a Boolean value indicating whether two values are equal. - /// - /// - Parameters: - /// - lhs: A value to compare. - /// - rhs: Another value to compare. - public static func == ( - lhs: WhatsNewViewController.Animation, - rhs: WhatsNewViewController.Animation - ) -> Bool { - // Switch on left-hand and right-hand side - switch (lhs, rhs) { - case (.fade, .fade): - return true - case (.slideUp, .slideUp): - return true - case (.slideDown, .slideDown): - return true - case (.slideLeft, .slideLeft): - return true - case (.slideRight, .slideRight): - return true - case (.custom, .custom): - return true - default: - return false - } - } - -} - -// MARK: - Animator Typealias - -public extension WhatsNewViewController.Animation { - - /// The Animator typealias closure with UIView and Item count - typealias Animator = (UIView, AnimatorSettings) -> Void - - /// The AnimatorSettings - struct AnimatorSettings: Codable, Equatable { - - /// The preferred duration - public let preferredDuration: TimeInterval - - /// The preferred delay - public let preferredDelay: TimeInterval - - /// Default initializer - /// - /// - Parameters: - /// - preferredDuration: The preferred duration - /// - preferredDelay: The preferred delay - public init( - preferredDuration: TimeInterval, - preferredDelay: TimeInterval - ) { - self.preferredDuration = preferredDuration - self.preferredDelay = preferredDelay - } - - } - -} - -// MARK: - Animator RawRepresentable - -extension WhatsNewViewController.Animation: RawRepresentable { - - /// Associated type RawValue as optional Animator - public typealias RawValue = WhatsNewViewController.Animation.Animator - - /// RawRepresentable initializer - /// - /// - Parameters: - /// - rawValue: The rawValue - public init?(rawValue: @escaping RawValue) { - self = .custom(animator: rawValue) - } - - /// The optional Animator rawValue - public var rawValue: RawValue { - switch self { - case .custom(animator: let animator): - // Return custom animator - return animator - case .fade, .slideUp, .slideDown, .slideLeft, .slideRight: - // Return predefined animation - return self.animate - } - } - -} - -// MARK: Predefined Animation - -private extension WhatsNewViewController.Animation { - - /// Predefined Animation - /// - /// - Parameters: - /// - view: The View - /// - index: The Index - func animate( - view: UIView, - animatorSettings: AnimatorSettings - ) { - // Declare Transform - let transform: CGAffineTransform - // Switch on self to initialize Transform - switch self { - case .slideUp: - transform = .init( - translationX: 0, - y: view.frame.size.height / 2 - ) - case .slideDown: - transform = .init( - translationX: 0, - y: view.frame.size.height / -2 - ) - case .slideLeft: - transform = .init( - translationX: view.frame.size.width, - y: 0 - ) - case .slideRight: - transform = .init( - translationX: -view.frame.size.width, - y: 0 - ) - default: - transform = .identity - } - // Apply Transform - view.transform = transform - // Set zero alpha - view.alpha = 0.0 - // Perform animation - UIView.animate( - // Duration - withDuration: animatorSettings.preferredDuration, - // Delay - delay: animatorSettings.preferredDelay, - // Ease in and out - options: .curveEaseInOut, - animations: { - // Set identity transform - view.transform = .identity - // Set default alpha - view.alpha = 1.0 - }) - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+CompletionButton.swift b/Sources/Configuration/WhatsNewViewController+CompletionButton.swift deleted file mode 100644 index 6fcc921..0000000 --- a/Sources/Configuration/WhatsNewViewController+CompletionButton.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// WhatsNewViewController+CompletionButton.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 27.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - CompletionButton - -public extension WhatsNewViewController { - - /// The CompletionButton - struct CompletionButton: Equatable { - - /// The Title - public var title: String - - /// The Action - public var action: Action - - /// The HapticFeedback - public var hapticFeedback: HapticFeedback? - - /// The background color - public var backgroundColor: UIColor - - /// The title font - public var titleFont: UIFont - - /// The title color - public var titleColor: UIColor - - /// The corner radius - public var cornerRadius: CGFloat - - /// The Animation - public var animation: Animation? - - /// The Insets - public var insets: UIEdgeInsets - - /// The Content Edge Insets - public var contentEdgeInsets: UIEdgeInsets - - /// Default initializer - /// - /// - Parameters: - /// - title: The Title. Default value `Continue` - /// - action: The Action. Default value `.dismiss` - /// - hapticFeedback: The optional HapticFeedback. Default value `nil` - /// - backgroundColor: The background color. Default value `.whatsNewKitBlue` - /// - titleFont: The title font. Default value `size: 17, weight: semibold` - /// - titleColor: The title color. Default value `white` - /// - cornerRadius: The corner radius. Default value `14.0` - /// - animation: The Animation. Default value `nil` - /// - insets: The UIEdgeInsets. Default value `top: 5, left: 23.5, bottom: 53.5, right: 23.5` - /// - contentEdgeInsets: The Content Edge UIEdgeInsets. Default value `top: 15, left: 0, bottom: 15, right: 0` - public init( - title: String = "Continue", - action: Action = .dismiss, - hapticFeedback: HapticFeedback? = nil, - backgroundColor: UIColor = .whatsNewKitBlue, - titleFont: UIFont = .systemFont(ofSize: 17, weight: .semibold), - titleColor: UIColor = .white, - cornerRadius: CGFloat = 14, - animation: Animation? = nil, - insets: UIEdgeInsets = .init(top: 5, left: 23.5, bottom: 53.5, right: 23.5), - contentEdgeInsets: UIEdgeInsets = .init(top: 14, left: 0, bottom: 14, right: 0) - ) { - self.title = title - self.action = action - self.hapticFeedback = hapticFeedback - self.backgroundColor = backgroundColor - self.titleFont = titleFont - self.titleColor = titleColor - self.cornerRadius = cornerRadius - self.animation = animation - self.insets = insets - self.contentEdgeInsets = contentEdgeInsets - } - } - -} - -// MARK: - ExpressibleByStringLiteral - -extension WhatsNewViewController.CompletionButton: ExpressibleByStringLiteral { - - /// Creates an instance initialized to the given string value. - /// - /// - Parameter value: The value of the new instance. - public init(stringLiteral value: String) { - self.init(title: value) - } - -} - -// MARK: - Action - -public extension WhatsNewViewController.CompletionButton { - - /// The CompletionButton Action - enum Action { - /// Dismiss - case dismiss - /// Perform custom completion action - case custom(action: (WhatsNewViewController) -> Void) - } - -} - -// MARK: - Equatable - -extension WhatsNewViewController.CompletionButton.Action: Equatable { - - /// Returns a Boolean value indicating whether two values are equal. - /// - /// - Parameters: - /// - lhs: A value to compare. - /// - rhs: Another value to compare. - public static func == (lhs: WhatsNewViewController.CompletionButton.Action, - rhs: WhatsNewViewController.CompletionButton.Action) -> Bool { - switch (lhs, rhs) { - case (.dismiss, .dismiss): - return true - case (.custom, .custom): - return true - default: - return false - } - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+Configuration.swift b/Sources/Configuration/WhatsNewViewController+Configuration.swift deleted file mode 100644 index 2d485f0..0000000 --- a/Sources/Configuration/WhatsNewViewController+Configuration.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// WhatsNewViewController+Configuration.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 24.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Configuration - -public extension WhatsNewViewController { - - /// The WhatsNewViewController Configuration - struct Configuration { - - // MARK: Properties - - /// Specifies whether the WhatsNewViewController prefers the status bar to be hidden or shown. - public var prefersStatusBarHidden: Bool - - /// The background color - public var backgroundColor: UIColor - - /// The TitleView - public var titleView: TitleView - - /// The ItemsView - public var itemsView: ItemsView - - /// The optional DetailButton - public var detailButton: DetailButton? - - /// The CompletionButton - public var completionButton: CompletionButton - - /// The iPad Adjustment Closure - public var padAdjustment: PadAdjustment - - /// The tint color based on completionButtonTheme backgroundcolor - public var tintColor: UIColor { - get { - return self.completionButton.backgroundColor - } - set { - self.detailButton?.titleColor = newValue - self.completionButton.backgroundColor = newValue - } - } - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - theme: The Theme. Default value `.default` - /// - prefersStatusBarHidden: Bool value if the status bar should be be hidden or shown. Default value `.false` - /// - backgroundColor: The background color. Default value `.whatsNewKitBackground` - /// - titleView: The TitleView. Default value `.init()` - /// - itemsView: The ItemsView. Default value `.init()` - /// - detailButton: The optional DetailButton. Default value `nil` - /// - completionButton: The completion button. Default value `.init()` - /// - padAdjustment: The The iPad Adjustment Closure. Default value `defaultPadAdjustment` - public init( - theme: Theme = .default, - prefersStatusBarHidden: Bool = false, - backgroundColor: UIColor = .whatsNewKitBackground, - titleView: TitleView = .init(), - itemsView: ItemsView = .init(), - detailButton: DetailButton? = nil, - completionButton: CompletionButton = .init(), - padAdjustment: @escaping PadAdjustment = Configuration.defaultPadAdjustment - ) { - self.prefersStatusBarHidden = prefersStatusBarHidden - self.backgroundColor = backgroundColor - self.titleView = titleView - self.itemsView = itemsView - self.detailButton = detailButton - self.completionButton = completionButton - self.padAdjustment = padAdjustment - self.apply(theme: theme) - } - - /// Convenience Initializer with Theme - /// - /// - Parameter theme: The Theme - public init(_ theme: Theme) { - self.init(theme: theme) - } - - } - -} - -// MARK: - PadAdjustment - -public extension WhatsNewViewController.Configuration { - - /// The Pad Adjustment Closure - typealias PadAdjustment = (inout WhatsNewViewController.Configuration) -> Void - - /// The default iPad Adjustment closure - static let defaultPadAdjustment: PadAdjustment = { configuration in - // Increase TitleView Insets - configuration.titleView.insets.top *= 1.5 - configuration.titleView.insets.left *= 2 - configuration.titleView.insets.right *= 2 - configuration.titleView.insets.bottom *= 2 - // Increase ItemsView Insets - configuration.itemsView.insets.top *= 2 - configuration.itemsView.insets.left *= 5 - configuration.itemsView.insets.right *= 5 - configuration.itemsView.insets.bottom *= 2 - // Increase CompletionButton Insets - configuration.completionButton.insets.top *= 4 - configuration.completionButton.insets.left *= 5 - configuration.completionButton.insets.right *= 5 - configuration.completionButton.insets.bottom *= 2.5 - } - -} - -// MARK: - Configuration Apply Functions - -public extension WhatsNewViewController.Configuration { - - /// Apply Theme to Configuration - /// - /// - Parameter theme: The Theme - /// - Returns: Discardable Configuration - @discardableResult - mutating func apply( - theme: WhatsNewViewController.Theme - ) -> WhatsNewViewController.Configuration { - // Perform Customization - theme.customization(&self) - // Return self - return self - } - - /// Apply a given Animation to all Views - /// - /// - Parameter animation: The Animation - /// - Returns: Discardable Configuration - @discardableResult - mutating func apply( - animation: WhatsNewViewController.Animation - ) -> WhatsNewViewController.Configuration { - self.titleView.animation = animation - self.itemsView.animation = animation - self.detailButton?.animation = animation - self.completionButton.animation = animation - return self - } - - /// Apply a given UIColor as text color to all Views - /// - /// - Parameter textColor: The text color - /// - Returns: Discardable Configuration - @discardableResult - mutating func apply( - textColor: UIColor - ) -> WhatsNewViewController.Configuration { - self.titleView.titleColor = textColor - self.itemsView.titleColor = textColor - self.itemsView.subtitleColor = textColor - return self - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+DetailButton.swift b/Sources/Configuration/WhatsNewViewController+DetailButton.swift deleted file mode 100644 index 2270fb1..0000000 --- a/Sources/Configuration/WhatsNewViewController+DetailButton.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// WhatsNewViewController+DetailButton.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 27.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - DetailButton - -public extension WhatsNewViewController { - - /// The DetailButton - struct DetailButton: Equatable { - - /// The Title - public var title: String - - /// The Action - public var action: Action - - /// The HapticFeedback - public var hapticFeedback: HapticFeedback? - - /// The title font - public var titleFont: UIFont - - /// The title color - public var titleColor: UIColor - - /// The Animation - public var animation: Animation? - - /// The Bottom Layout Offset - public var bottomOffset: CGFloat - - /// Default initializer - /// - /// - Parameters: - /// - title: The Title - /// - action: The Action - /// - hapticFeedback: The optional HapticFeedback - /// - titleFont: The title font. Default value `size: 17` - /// - titleColor: The title color. Default value `.whatsNewKitBlue` - /// - animation: The Animation. Default value `nil` - /// - bottomOffset: The Bottom Layout Offset. Default value `10` - public init( - title: String, - action: Action, - hapticFeedback: HapticFeedback? = nil, - titleFont: UIFont = .systemFont(ofSize: 17), - titleColor: UIColor = .whatsNewKitBlue, - animation: Animation? = nil, - bottomOffset: CGFloat = 10 - ) { - self.title = title - self.action = action - self.hapticFeedback = hapticFeedback - self.titleFont = titleFont - self.titleColor = titleColor - self.animation = animation - self.bottomOffset = bottomOffset - } - - } - -} - -// MARK: - Action - -public extension WhatsNewViewController.DetailButton { - - /// The DetailButton Action - enum Action { - /// Present Website on URL - case website(url: String) - /// Perform custom detail action - case custom(action: (WhatsNewViewController) -> Void) - } - -} - -// MARK: - Equatable - -extension WhatsNewViewController.DetailButton.Action: Equatable { - - /// Returns a Boolean value indicating whether two values are equal. - /// - /// - Parameters: - /// - lhs: A value to compare. - /// - rhs: Another value to compare. - public static func == ( - lhs: WhatsNewViewController.DetailButton.Action, - rhs: WhatsNewViewController.DetailButton.Action - ) -> Bool { - switch (lhs, rhs) { - case (.website, .website): - return true - case (.custom, .custom): - return true - default: - return false - } - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+HapticFeedback.swift b/Sources/Configuration/WhatsNewViewController+HapticFeedback.swift deleted file mode 100644 index 324bad7..0000000 --- a/Sources/Configuration/WhatsNewViewController+HapticFeedback.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// WhatsNewViewController+HapticFeedback.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 28.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - HapticFeedback - -public extension WhatsNewViewController { - - /// The HapticFeedback - enum HapticFeedback: Equatable { - /// ImpactFeedback with FeedbackStyle - case impact(ImpactFeedbackStyle) - /// SelectionFeedback - case selection - /// NotificationFeedback with FeedbackType - case notification(NotificationFeedbackType) - } - -} - -// MARK: - Execute - -extension WhatsNewViewController.HapticFeedback { - - /// Execute HapticFeedback - func execute() { - // Check if iOS 10 or greater is available - if #available(iOS 10.0, *) { - // Switch on self - switch self { - case .impact(let style): - // UIFeedbackGenerator - let feedbackGenerator = UIImpactFeedbackGenerator(style: style.rawStyle) - feedbackGenerator.impactOccurred() - case .selection: - // UISelectionFeedbackGenerator - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() - case .notification(let type): - // UINotificationFeedbackGenerator - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - notificationFeedbackGenerator.notificationOccurred(type.rawType) - } - } - } - -} - -// MARK: - ImpactFeedbackStyle - -public extension WhatsNewViewController.HapticFeedback { - - /// The ImpactFeedbackStyle - enum ImpactFeedbackStyle: String, Codable, Equatable, Hashable, CaseIterable { - /// Light - case light - /// Medium - case medium - /// Heavy - case heavy - } - -} - -// MARK: - ImpactFeedbackStyle+RawStyle - -extension WhatsNewViewController.HapticFeedback.ImpactFeedbackStyle { - - /// The Raw Style - @available(iOS 10.0, *) - var rawStyle: UIImpactFeedbackGenerator.FeedbackStyle { - switch self { - case .light: - return .light - case .medium: - return .medium - case .heavy: - return .heavy - } - } - -} - -// MARK: - NotificationFeedbackType - -public extension WhatsNewViewController.HapticFeedback { - - /// The NotificationFeedbackType - enum NotificationFeedbackType: String, Codable, Equatable, Hashable, CaseIterable { - /// Success - case success - /// Warning - case warning - /// Error - case error - } - -} - -// MARK: - NotificationFeedbackType+RawType - -extension WhatsNewViewController.HapticFeedback.NotificationFeedbackType { - - /// The Raw Type - @available(iOS 10.0, *) - var rawType: UINotificationFeedbackGenerator.FeedbackType { - switch self { - case .success: - return .success - case .warning: - return .warning - case .error: - return .error - } - } - -} diff --git a/Sources/Configuration/WhatsNewViewController+ItemsView.swift b/Sources/Configuration/WhatsNewViewController+ItemsView.swift deleted file mode 100644 index bfbe193..0000000 --- a/Sources/Configuration/WhatsNewViewController+ItemsView.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// WhatsNewViewController+ItemsView.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 06.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - ItemsView - -public extension WhatsNewViewController { - - /// The ItemsViewTheme - struct ItemsView: Equatable { - - /// The title font - public var titleFont: UIFont - - /// The title color - public var titleColor: UIColor - - /// The subtitle font - public var subtitleFont: UIFont - - /// The subtitle color - public var subtitleColor: UIColor - - /// The ImageSize - public var imageSize: ImageSize - - /// Boolean if Image should be auto tinted - public var autoTintImage: Bool - - /// The Layout - public var layout: Layout - - /// The ContentMode - public var contentMode: ContentMode - - /// The Animation - public var animation: Animation? - - /// The Insets - public var insets: UIEdgeInsets - - /// Default initializer - /// - /// - Parameters: - /// - titleFont: The title font. Default value `size: 17, weight: semibold` - /// - titleColor: The title color. Default value `.whatsNewKitForeground` - /// - subtitleFont: The subtitle font. Default value `size: 17` - /// - subtitleColor: The subtitle color. Default value `.whatsNewKitForeground` - /// - imageSize: The ImageSize. Default value `preferred` - /// - autoTintImage: The autoTintImage boolean. Default value `true` - /// - layout: The Layout. Default value `left` - /// - contentMode: The ContentMode. Default value `top` - /// - animation: The Animation. Default value `nil` - /// - insets: The UIEdgeInsets. Default value `top: 15, left: 20, bottom: 5, right: 20` - public init( - titleFont: UIFont = .systemFont(ofSize: 17, weight: .semibold), - titleColor: UIColor = .whatsNewKitForeground, - subtitleFont: UIFont = .systemFont(ofSize: 17), - subtitleColor: UIColor = .whatsNewKitForeground, - imageSize: ImageSize = .preferred, - autoTintImage: Bool = true, - layout: Layout = .left, - contentMode: ContentMode = .top, - animation: Animation? = nil, - insets: UIEdgeInsets = .init(top: 15, left: 20, bottom: 5, right: 20) - ) { - self.titleFont = titleFont - self.titleColor = titleColor - self.subtitleFont = subtitleFont - self.subtitleColor = subtitleColor - self.imageSize = imageSize - self.autoTintImage = autoTintImage - self.layout = layout - self.contentMode = contentMode - self.animation = animation - self.insets = insets - } - - } - -} - -// MARK: - ItemsView.Layout - -public extension WhatsNewViewController.ItemsView { - - /// The Layout - enum Layout: String, Codable, Equatable, Hashable, CaseIterable { - /// Left image and right aligned text - case left - /// Centered image and centered text - case centered - /// Right image and left aligned text - case right - } - -} - -// MARK: - ItemsView.ContentMode - -public extension WhatsNewViewController.ItemsView { - - /// The ContentMode - enum ContentMode: String, Codable, Equatable, Hashable, CaseIterable { - /// Top - case top - /// Center - case center - /// Fill - case fill - } - -} - -// MARK: - ItemsView.ImageSize - -public extension WhatsNewViewController.ItemsView { - - /// The ImageSize - enum ImageSize: Equatable, Hashable { - /// The original Image Size - case original - /// A fixed size square which contains the image, with a preserved aspect ratio - case fixed(height: Double) - } - -} - -// MARK: - ItemsView.ImageSize+preferred - -public extension WhatsNewViewController.ItemsView.ImageSize { - - /// The preferred Image Size `.fixed(height: 50)` - static let preferred: WhatsNewViewController.ItemsView.ImageSize = .fixed(height: 50) - -} diff --git a/Sources/Configuration/WhatsNewViewController+TitleView.swift b/Sources/Configuration/WhatsNewViewController+TitleView.swift deleted file mode 100644 index 713c601..0000000 --- a/Sources/Configuration/WhatsNewViewController+TitleView.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// WhatsNewViewController+TitleView.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 06.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - TitleView - -public extension WhatsNewViewController { - - /// The TitleView - struct TitleView: Equatable { - - /// The TitleMode - public var titleMode: TitleMode - - /// The title font - public var titleFont: UIFont - - /// The title color - public var titleColor: UIColor - - /// The title alignment - public var titleAlignment: NSTextAlignment - - /// The Animation - public var animation: Animation? - - /// The SecondaryColor - public var secondaryColor: SecondaryColor? - - /// The Insets - public var insets: UIEdgeInsets - - /// Default initializer - /// - /// - Parameters: - /// - titleMode: The TitleMode. Default value `.fixed` - /// - titleFont: The title font. Default value `size: 35, weight: semibold` - /// - titleColor: The title color. Default value `.whatsNewKitForeground` - /// - titleAlignment: The title alignment. Default value `center` - /// - animation: The Animation. Default value `nil` - /// - secondaryColor: The SecondaryColor. Default value `nil` - /// - insets: The UIEdgeInsets. Default value `top: 80, left: 20, bottom: 27, right: 20` - public init( - titleMode: TitleMode = .fixed, - titleFont: UIFont = .systemFont(ofSize: 35, weight: .semibold), - titleColor: UIColor = .whatsNewKitForeground, - titleAlignment: NSTextAlignment = .center, - animation: Animation? = nil, - secondaryColor: SecondaryColor? = nil, - insets: UIEdgeInsets = .init(top: 80, left: 20, bottom: 27, right: 20) - ) { - self.titleMode = titleMode - self.titleFont = titleFont - self.titleColor = titleColor - self.titleAlignment = titleAlignment - self.animation = animation - self.secondaryColor = secondaryColor - self.insets = insets - } - - } - -} - -// MARK: - TitleMode - -public extension WhatsNewViewController.TitleView { - - /// Used to describe how the title view should be styled. - enum TitleMode: String, Codable, Equatable, Hashable, CaseIterable { - /// The title view should remain fixed at the top, regardless of scrolling - case fixed - /// The title view should scroll with the rest of the items (out of view) - case scrolls - } - -} - -// MARK: - SecondaryColor - -public extension WhatsNewViewController.TitleView { - - /// The SecondaryColor - struct SecondaryColor: Equatable { - - /// The start index - var startIndex: Int - - /// The length - var length: Int - - /// The color - var color: UIColor - - /// Designated Initializer - /// - /// - Parameters: - /// - startIndex: The start index - /// - length: The length - /// - color: The color - public init( - startIndex: Int, - length: Int, - color: UIColor - ) { - self.startIndex = startIndex - self.length = length - self.color = color - } - - } - -} diff --git a/Sources/DataStructures/Disposable/Disposable.swift b/Sources/DataStructures/Disposable/Disposable.swift deleted file mode 100644 index e239feb..0000000 --- a/Sources/DataStructures/Disposable/Disposable.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Disposable.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 27.09.20. -// Copyright © 2020 WhatsNewKit. All rights reserved. -// - -import Foundation - -// MARK: - Disposable - -/// A Diposable Type -protocol Disposable { - - /// Dispose - func dispose() - -} - -// MARK: - Store In - -extension Disposable { - - /// Store the Disposable in a given DisposeBag - /// - Parameter disposeBag: The DisposeBag where the Disposable should be stored in - /// - Returns: The Disposable - @discardableResult - func store(in disposeBag: DisposeBag) -> Disposable { - // Append Disposable to DisposeBag - disposeBag.store(self) - // Return Disposable - return self - } - -} - -// MARK: - ClosureDisposable - -/// A ClosureDisposable -struct ClosureDisposable: Disposable { - - // MARK: Properties - - /// The dispose closure - private let closure: () -> Void - - // MARK: Initializer - - /// Designated Initializer - /// - Parameter closure: The dispose closure - init(closure: @escaping () -> Void) { - self.closure = closure - } - - // MARK: Disposable - - /// Dispose - func dispose() { - self.closure() - } - -} diff --git a/Sources/DataStructures/Disposable/DisposeBag.swift b/Sources/DataStructures/Disposable/DisposeBag.swift deleted file mode 100644 index c3a7a7a..0000000 --- a/Sources/DataStructures/Disposable/DisposeBag.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DisposeBag.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 27.09.20. -// Copyright © 2020 WhatsNewKit. All rights reserved. -// - -import Foundation - -// MARK: - DisposeBag - -/// A DisposeBag -final class DisposeBag { - - // MARK: Properties - - /// The Disposables - private var disposables: [Disposable] - - // MARK: Initializer - - /// Designated Initializer - /// - Parameter disposables: The Disposables. Default vaue `.init()` - init(disposables: [Disposable] = .init()) { - self.disposables = disposables - } - -} - -// MARK: - Disposable - -extension DisposeBag: Disposable { - - /// Dispose - func dispose() { - self.disposables.forEach { $0.dispose() } - self.disposables.removeAll() - } - -} - -// MARK: - Store - -extension DisposeBag { - - /// Store a given Disposable - /// - Parameter disposable: The Disposable that should be stored - func store(_ disposable: Disposable) { - self.disposables.append(disposable) - } - -} diff --git a/Sources/Environment/WhatsNewEnvironment+Key.swift b/Sources/Environment/WhatsNewEnvironment+Key.swift new file mode 100644 index 0000000..f868204 --- /dev/null +++ b/Sources/Environment/WhatsNewEnvironment+Key.swift @@ -0,0 +1,31 @@ +import SwiftUI + +// MARK: - WhatsNewEnvironment+Key + +public extension WhatsNewEnvironment { + + /// The WhatsNewEnvironment Key + enum Key: EnvironmentKey { + + /// The default value for the environment key + public static var defaultValue = WhatsNewEnvironment() + + } + +} + +// MARK: - EnvironmentValues+whatsNew + +public extension EnvironmentValues { + + /// The WhatsNewEnvironment + var whatsNew: WhatsNewEnvironment { + get { + self[WhatsNewEnvironment.Key.self] + } + set { + self[WhatsNewEnvironment.Key.self] = newValue + } + } + +} diff --git a/Sources/Environment/WhatsNewEnvironment.swift b/Sources/Environment/WhatsNewEnvironment.swift new file mode 100644 index 0000000..ec04999 --- /dev/null +++ b/Sources/Environment/WhatsNewEnvironment.swift @@ -0,0 +1,114 @@ +import Foundation + +// MARK: - WhatsNewEnvironment + +/// A WhatsNew Environment +open class WhatsNewEnvironment { + + // MARK: Properties + + /// The current WhatsNew Version + public let currentVersion: WhatsNew.Version + + /// The WhatsNewVersionStore + public let whatsNewVersionStore: WhatsNewVersionStore + + /// The default WhatsNew Layout + public let defaultLayout: WhatsNew.Layout + + /// The WhatsNewCollection + public let whatsNewCollection: WhatsNewCollection + + // MARK: Initializer + + /// Creates a new instance of `WhatsNewEnvironment` + /// - Parameters: + /// - currentVersion: The current WhatsNew Version. Default value `.current()` + /// - versionStore: The WhatsNewVersionStore. Default value `UserDefaultsWhatsNewVersionStore()` + /// - defaultLayout: The default WhatsNew Layout. Default value `.default` + /// - whatsNewCollection: The WhatsNewCollection + public init( + currentVersion: WhatsNew.Version = .current(), + versionStore: WhatsNewVersionStore = UserDefaultsWhatsNewVersionStore(), + defaultLayout: WhatsNew.Layout = .default, + whatsNewCollection: WhatsNewCollection = .init() + ) { + self.currentVersion = currentVersion + self.whatsNewVersionStore = versionStore + self.defaultLayout = defaultLayout + self.whatsNewCollection = whatsNewCollection + } + + /// Creates a new instance of `WhatsNewEnvironment` + /// - Parameters: + /// - currentVersion: The current WhatsNew Version. Default value `.current()` + /// - versionStore: The WhatsNewVersionStore. Default value `UserDefaultsWhatsNewVersionStore()` + /// - defaultLayout: The default WhatsNew Layout. Default value `.default` + /// - whatsNewCollection: The WhatsNewCollectionProvider + public convenience init( + currentVersion: WhatsNew.Version = .current(), + versionStore: WhatsNewVersionStore = UserDefaultsWhatsNewVersionStore(), + defaultLayout: WhatsNew.Layout = .default, + whatsNewCollection whatsNewCollectionProvider: WhatsNewCollectionProvider + ) { + self.init( + currentVersion: currentVersion, + versionStore: versionStore, + defaultLayout: defaultLayout, + whatsNewCollection: whatsNewCollectionProvider.whatsNewCollection + ) + } + + /// Creates a new instance of `WhatsNewEnvironment` + /// - Parameters: + /// - currentVersion: The current WhatsNew Version. Default value `.current()` + /// - versionStore: The WhatsNewVersionStore. Default value `UserDefaultsWhatsNewVersionStore()` + /// - defaultLayout: The default WhatsNew Layout. Default value `.default` + /// - whatsNewCollection: A result builder closure that produces a WhatsNewCollection + public convenience init( + currentVersion: WhatsNew.Version = .current(), + versionStore: WhatsNewVersionStore = UserDefaultsWhatsNewVersionStore(), + defaultLayout: WhatsNew.Layout = .default, + @WhatsNewCollectionBuilder + whatsNewCollection: () -> WhatsNewCollection + ) { + self.init( + currentVersion: currentVersion, + versionStore: versionStore, + defaultLayout: defaultLayout, + whatsNewCollection: whatsNewCollection() + ) + } + + // MARK: WhatsNew + + /// Retrieve a WhatsNew that should be presented to the user, if available. + open func whatsNew() -> WhatsNew? { + // Retrieve presented WhatsNew Versions from WhatsNewVersionStore + let presentedWhatsNewVersions = self.whatsNewVersionStore.presentedVersions + // Verify the current Version has not been presented + guard !presentedWhatsNewVersions.contains(self.currentVersion) else { + // Otherwise WhatsNew has already been presented for the current version + return nil + } + // Check if a WhatsNew is available for the current Version + if let whatsNew = self.whatsNewCollection.first(where: { $0.version == self.currentVersion }) { + // Return WhatsNew for the current Version + return whatsNew + } + // Otherwise initialize current minor release Version + let currentMinorVersion = WhatsNew.Version( + major: self.currentVersion.major, + minor: self.currentVersion.minor, + patch: 0 + ) + // Verify the current minor release Version has not been presented + guard !presentedWhatsNewVersions.contains(currentMinorVersion) else { + // Otherwise WhatsNew for current minor release Version has already been preseted + return nil + } + // Return WhatsNew for current minor release Version, if available + return self.whatsNewCollection.first { $0.version == currentMinorVersion } + } + +} diff --git a/Sources/Extensions/Anchor.swift b/Sources/Extensions/Anchor.swift deleted file mode 100644 index 8697f49..0000000 --- a/Sources/Extensions/Anchor.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Anchor.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 02.02.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Anchor - -/// The Anchor Protocol -protocol Anchor { - - /// The Top Anchor - var topAnchor: NSLayoutYAxisAnchor { get } - - /// The Bottom Anchor - var bottomAnchor: NSLayoutYAxisAnchor { get } - - /// The Leading Anchor - var leadingAnchor: NSLayoutXAxisAnchor { get } - - /// The Trailing Anchor - var trailingAnchor: NSLayoutXAxisAnchor { get } - - /// The Width Anchor - var widthAnchor: NSLayoutDimension { get } - - /// The Height Anchor - var heightAnchor: NSLayoutDimension { get } - - /// The center x anchor - var centerXAnchor: NSLayoutXAxisAnchor { get } - - /// The center y anchor - var centerYAnchor: NSLayoutYAxisAnchor { get } - -} - -// MARK: - UIView+Anchor - -extension UIView: Anchor {} - -// MARK: - UILayoutGuide+Anchor - -extension UILayoutGuide: Anchor {} - -// MARK: - UIViewController+Anchor - -extension UIViewController { - - /// The Anchor - var anchor: Anchor { - if #available(iOS 11.0, *) { - return self.view.safeAreaLayoutGuide - } else { - return self.view - } - } - -} diff --git a/Sources/Extensions/ScrollView+alwaysBounceVertical.swift b/Sources/Extensions/ScrollView+alwaysBounceVertical.swift new file mode 100644 index 0000000..85d1876 --- /dev/null +++ b/Sources/Extensions/ScrollView+alwaysBounceVertical.swift @@ -0,0 +1,113 @@ +#if os(iOS) +import SwiftUI + +// MARK: - ScrollView+alwaysBounceVertical + +extension ScrollView { + + /// Resolve the underlying `UIScrollView` to update the `alwaysBounceVertical` attribute which is + /// a Boolean value that determines whether bouncing always occurs when vertical scrolling reaches the end of the content. + /// - Parameter alwaysBounceVertical: Bool value if the UIScrollView should always bounce vertical + func alwaysBounceVertical( + _ alwaysBounceVertical: Bool + ) -> some View { + self.overlay( + ViewControllerResolver { viewController in + // Verify UIScrollView is available + guard let scrollView = viewController + .view + .subviews + .first(where: { $0 is UIScrollView }) as? UIScrollView else { + // Otherwise return out of function + return + } + // Set alwaysBounceVertical + scrollView.alwaysBounceVertical = alwaysBounceVertical + } + .frame(width: 0, height: 0) + ) + } + +} + +// MARK: - ViewControllerResolver + +/// The ViewControllerResolver +private struct ViewControllerResolver: UIViewControllerRepresentable { + + // MARK: Typealias + + /// A typealias represents a UIViewController resolver closure + typealias Resolver = (UIViewController) -> Void + + // MARK: Properties + + /// The Resolver + let resolver: Resolver + + // MARK: UIViewControllerRepresentable + + /// Make ResolvedViewController + /// - Parameter context: The Context + func makeUIViewController( + context: Context + ) -> Content { + .init(resolver: self.resolver) + } + + /// Update ResolvedViewController + /// - Parameters: + /// - uiViewController: The ResolvedViewController + /// - context: The Context + func updateUIViewController( + _ content: Content, + context: Context + ) { + content.resolver = self.resolver + } + +} + +// MARK: - ViewControllerResolver+Content + +private extension ViewControllerResolver { + + /// The ViewControllerResolver Content + final class Content: UIViewController { + + // MARK: Properties + + /// The Resolver + var resolver: Resolver + + // MARK: Initializer + + /// Creates a new instance of `ViewControllerResolver.Content` + /// - Parameter onResolve: The Resolver + init( + resolver: @escaping Resolver + ) { + self.resolver = resolver + super.init(nibName: nil, bundle: nil) + } + + /// Initializer with NSCoder is unavailable + @available(*, unavailable) + required init?( + coder aDecoder: NSCoder + ) { nil } + + // MARK: View-Lifecycle + + /// Did move to parent ViewController + /// - Parameter parent: The parent ViewController + override func didMove( + toParent parent: UIViewController? + ) { + super.didMove(toParent: parent) + parent.flatMap(self.resolver) + } + } + +} +#endif diff --git a/Sources/Extensions/Text+WhatsNewText.swift b/Sources/Extensions/Text+WhatsNewText.swift new file mode 100644 index 0000000..702864d --- /dev/null +++ b/Sources/Extensions/Text+WhatsNewText.swift @@ -0,0 +1,28 @@ +import SwiftUI + +// MARK: - Text+init(whatsNewText:) + +extension Text { + + /// Creates a new instance of `Text` from a `WhatsNew.Text` instance + /// - Parameter whatsNewText: The WhatsNew Text + init( + whatsNewText: WhatsNew.Text + ) { + // Check if iOS 15 or greater is available + if #available(iOS 15.0, macOS 12.0, *) { + // Initialize with AttributedString + self.init( + AttributedString( + whatsNewText.attributedString + ) + ) + } else { + // Initialize with raw string value + self.init( + verbatim: whatsNewText.attributedString.string + ) + } + } + +} diff --git a/Sources/Extensions/UIColor+Defaults.swift b/Sources/Extensions/UIColor+Defaults.swift deleted file mode 100644 index f58efc7..0000000 --- a/Sources/Extensions/UIColor+Defaults.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// UIColor+Defaults.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 25.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - isLight - -extension UIColor { - - /// Retrieve Boolean if UIColor is light - var isLight: Bool { - var white: CGFloat = 0 - self.getWhite(&white, alpha: nil) - return white > 0.5 - } - -} - -// MARK: - Template Colors - -public extension UIColor { - - /// The WhatsNewKit background color - static var whatsNewKitBackground: UIColor { - if #available(iOS 13.0, *) { - return UIColor { traitCollection in - switch traitCollection.userInterfaceStyle { - case .light, .unspecified: - return .whatsNewKitWhite - case .dark: - return .whatsNewKitDark - @unknown default: - return .whatsNewKitWhite - } - } - } else { - return .whatsNewKitWhite - } - } - - /// The WhatsNewKit background color - static var whatsNewKitForeground: UIColor { - if #available(iOS 13.0, *) { - return UIColor { traitCollection in - switch traitCollection.userInterfaceStyle { - case .light, .unspecified: - return .whatsNewKitBlack - case .dark: - return .whatsNewKitWhite - @unknown default: - return .whatsNewKitBlack - } - } - } else { - return .whatsNewKitBlack - } - } - - /// The WhatsNewKit white color - static let whatsNewKitWhite = UIColor( - red: 1, - green: 1, - blue: 1, - alpha: 1 - ) - - /// The WhatsNewKit black color - static let whatsNewKitBlack = UIColor( - red: 0, - green: 0, - blue: 0, - alpha: 1 - ) - - /// The WhatsNewKit blue color - static let whatsNewKitBlue = UIColor( - red: 0, - green: 122 / 255, - blue: 1, - alpha: 1 - ) - - /// The WhatsNewKit light blue color - static let whatsNewKitLightBlue = UIColor( - red: 95 / 255, - green: 200 / 255, - blue: 248 / 255, - alpha: 1 - ) - - /// The WhatsNewKit dark color - static let whatsNewKitDark = UIColor( - red: 20 / 255, - green: 29 / 255, - blue: 38 / 255, - alpha: 1 - ) - - /// The WhatsNewKit purple color - static let whatsNewKitPurple = UIColor( - red: 183 / 255, - green: 35 / 255, - blue: 1, - alpha: 1 - ) - - /// The WhatsNewKit red color - static let whatsNewKitRed = UIColor( - red: 1, - green: 45 / 255, - blue: 85 / 255, - alpha: 1 - ) - - /// The WhatsNewKit green color - static let whatsNewKitGreen = UIColor( - red: 76 / 255, - green: 217 / 255, - blue: 100 / 255, - alpha: 1 - ) - -} diff --git a/Sources/Extensions/UIView+MakeConstraints.swift b/Sources/Extensions/UIView+MakeConstraints.swift deleted file mode 100644 index b40fe88..0000000 --- a/Sources/Extensions/UIView+MakeConstraints.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// UIView+MakeConstraints.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 11.04.20. -// Copyright © 2020 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Make Constraints - -extension UIView { - - /// Make Constraints - /// - Parameter constraints: The NSLayoutConstraints that should be added to the View - @discardableResult - func makeConstraints(_ constraints: NSLayoutConstraint...) -> Disposable { - // Disable translates Autoresizing Mask Into Constraints - self.translatesAutoresizingMaskIntoConstraints = false - // Activate the given Constraints - NSLayoutConstraint.activate(constraints) - // Return ClosureDisposable - return ClosureDisposable { [weak self] in - // Deactivate all Constraints - constraints.forEach { $0.isActive = false } - // Remove all Constraints - constraints.forEach { self?.removeConstraint($0) } - } - } - -} diff --git a/Sources/Extensions/UIVisualEffectView+Representable.swift b/Sources/Extensions/UIVisualEffectView+Representable.swift new file mode 100644 index 0000000..d15a7da --- /dev/null +++ b/Sources/Extensions/UIVisualEffectView+Representable.swift @@ -0,0 +1,42 @@ +#if os(iOS) +import SwiftUI + +// MARK: - UIVisualEffectView+Representable + +extension UIVisualEffectView { + + /// A UIVisualEffect SwiftUI Representable View + struct Representable: UIViewRepresentable { + + // MARK: Properties + + /// The UIVisualEffect. Default value `UIBlurEffect(style: .regular)` + var effect: UIVisualEffect = UIBlurEffect(style: .regular) + + // MARK: UIViewRepresentable + + /// Make UIVisualEffectView + /// - Parameter context: The Context + func makeUIView( + context: Context + ) -> UIVisualEffectView { + .init( + effect: self.effect + ) + } + + /// Update UIVisualEffectView + /// - Parameters: + /// - visualEffectView: The UIVisualEffectView + /// - context: The Context + func updateUIView( + _ visualEffectView: UIVisualEffectView, + context: Context + ) { + visualEffectView.effect = self.effect + } + + } + +} +#endif diff --git a/Sources/Extensions/View+WhatsNewSheet.swift b/Sources/Extensions/View+WhatsNewSheet.swift new file mode 100644 index 0000000..0bb687f --- /dev/null +++ b/Sources/Extensions/View+WhatsNewSheet.swift @@ -0,0 +1,155 @@ +import SwiftUI + +// MARK: - View+sheet(whatsNew:) + +public extension View { + + /// Presents a WhatsNewView using the given WhatsNew object as a data source for the sheet’s content. + /// - Parameters: + /// - whatsNew: A Binding to an optional WhatsNew object + /// - versionStore: The optional WhatsNewVersionStore. Default value `nil` + /// - layout: The WhatsNew Layout. Default value `.default` + /// - onDimiss: The closure to execute when dismissing the sheet. Default value `nil` + func sheet( + whatsNew: Binding, + versionStore: WhatsNewVersionStore? = nil, + layout: WhatsNew.Layout = .default, + onDimiss: (() -> Void)? = nil + ) -> some View { + self.modifier( + ManualWhatsNewSheetViewModifier( + whatsNew: whatsNew, + versionStore: versionStore, + layout: layout, + onDismiss: onDimiss + ) + ) + } + +} + +// MARK: - ManualWhatsNewSheetViewModifier + +/// A Manual WhatsNew Sheet ViewModifier +private struct ManualWhatsNewSheetViewModifier: ViewModifier { + + // MARK: Properties + + /// A Binding to an optional WhatsNew object + let whatsNew: Binding + + /// The optional WhatsNewVersionStore + let versionStore: WhatsNewVersionStore? + + /// The WhatsNew Layout + let layout: WhatsNew.Layout + + /// The closure to execute when dismissing the sheet + let onDismiss: (() -> Void)? + + // MARK: ViewModifier + + /// Gets the current body of the caller. + /// - Parameter content: The Content + func body( + content: Content + ) -> some View { + // Check if a WhatsNew object is available + if let whatsNew = self.whatsNew.wrappedValue { + // Check if the WhatsNew Version has already been presented + if self.versionStore?.hasPresented(whatsNew.version) == true { + // Show content + content + } else { + // Show WhatsNew Sheet + content.sheet( + item: self.whatsNew, + onDismiss: self.onDismiss + ) { whatsNew in + WhatsNewView( + whatsNew: whatsNew, + versionStore: self.versionStore, + layout: self.layout + ) + } + } + } else { + // Otherwise show content + content + } + } + +} + +// MARK: - View+whatsNewSheet() + +public extension View { + + /// Auto-Presents a WhatsNewView to the user if needed based on the `WhatsNewEnvironment` + /// - Parameters: + /// - layout: The optional custom WhatsNew Layout. Default value `nil` + /// - onDimiss: The closure to execute when dismissing the sheet. Default value `nil` + func whatsNewSheet( + layout: WhatsNew.Layout? = nil, + onDismiss: (() -> Void)? = nil + ) -> some View { + self.modifier( + AutomaticWhatsNewSheetViewModifier( + layout: layout, + onDismiss: onDismiss + ) + ) + } + +} + +// MARK: - WhatsNewSheetViewModifier + +/// A Automatic WhatsNew Sheet ViewModifier +private struct AutomaticWhatsNewSheetViewModifier: ViewModifier { + + // MARK: Properties + + /// The optional WhatsNew Layout + let layout: WhatsNew.Layout? + + /// The optional closure to execute when dismissing the sheet + let onDismiss: (() -> Void)? + + /// Bool value if sheet is dismissed + @State + private var isDismissed: Bool? + + /// The WhatsNewEnvironment + @Environment(\.whatsNew) + private var whatsNewEnvironment + + // MARK: ViewModifier + + /// Gets the current body of the caller. + /// - Parameter content: The Content + func body( + content: Content + ) -> some View { + content.sheet( + item: .init( + get: { + self.isDismissed == true + ? nil + : self.whatsNewEnvironment.whatsNew() + }, + set: { + self.isDismissed = $0 == nil + } + ), + onDismiss: self.onDismiss + ) { whatsNew in + WhatsNewView( + whatsNew: whatsNew, + versionStore: self.whatsNewEnvironment.whatsNewVersionStore, + layout: self.layout ?? self.whatsNewEnvironment.defaultLayout + ) + } + } + +} diff --git a/Sources/Extensions/WhatsNew+Version+Key.swift b/Sources/Extensions/WhatsNew+Version+Key.swift new file mode 100644 index 0000000..836e511 --- /dev/null +++ b/Sources/Extensions/WhatsNew+Version+Key.swift @@ -0,0 +1,20 @@ +import Foundation + +// MARK: - WhatsNew.Version+key + +extension WhatsNew.Version { + + /// The WhatsNew Version Key prefix + static let keyPrefix = "WhatsNewKit" + + /// A WhatsNew Version Key the can be used to save + /// a WhatsNew Version to the `UserDefaults` or `NSUbiquitousKeyValueStore` + var key: String { + [ + Self.keyPrefix, + self.description + ] + .joined(separator: ".") + } + +} diff --git a/Sources/Models/WhatsNew+Feature+Image.swift b/Sources/Models/WhatsNew+Feature+Image.swift new file mode 100644 index 0000000..a438414 --- /dev/null +++ b/Sources/Models/WhatsNew+Feature+Image.swift @@ -0,0 +1,125 @@ +import SwiftUI + +// MARK: - WhatsNew+Item+Image + +public extension WhatsNew.Feature { + + /// A WhatsNew Feature Image + struct Image { + + // MARK: Properties + + /// A closure that produces the Image View + public let view: () -> AnyView + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Feature.Image` + /// - Parameters: + /// - image: A ViewBuilder closure that produces an Image View + public init( + @ViewBuilder + image: @escaping () -> Image + ) { + self.view = { .init(image()) } + } + + } + +} + +// MARK: - Image+init(image:) + +public extension WhatsNew.Feature.Image { + + /// Creates a new instance of `WhatsNew.Feature.Image` + /// - Parameter image: The Image + init( + image: Image + ) { + self.init { image } + } + +} + +// MARK: - Image+init(name:) + +public extension WhatsNew.Feature.Image { + + /// Creates a new instance of `WhatsNew.Feature.Image` + /// - Parameters: + /// - name: The name of the image resource to lookup + /// - bundle: The bundle to search for the image resource. Default value `.main` + /// - renderingMode: The mode SwiftUI uses to render images. Default value `.template` + /// - foregroundColor: The foreground color to use when displaying this view. Default value `.accentColor` + init( + name: String, + bundle: Bundle = .main, + renderingMode: Image.TemplateRenderingMode? = .template, + foregroundColor: Color? = .accentColor + ) { + self.init { + Image( + name, + bundle: bundle + ) + .renderingMode(renderingMode) + .font(.title) + .imageScale(.large) + .foregroundColor(foregroundColor) + } + } + +} + +// MARK: - Image+init(systemName:) + +public extension WhatsNew.Feature.Image { + + /// Creates a new instance of `WhatsNew.Feature.Image` + /// - Parameters: + /// - systemName: The name of the system symbol image + /// - renderingMode: The mode SwiftUI uses to render images. Default value `.template` + /// - foregroundColor: The foreground color to use when displaying this view. Default value `.accentColor` + init( + systemName: String, + renderingMode: Image.TemplateRenderingMode? = .template, + foregroundColor: Color? = .accentColor + ) { + self.init { + Image( + systemName: systemName + ) + .renderingMode(renderingMode) + .font(.title) + .imageScale(.large) + .foregroundColor(foregroundColor) + } + } + + /// Creates a new instance of `WhatsNew.Feature.Image` + /// - Parameters: + /// - systemName: The name of the system symbol image + /// - renderingMode: The mode SwiftUI uses to render images. Default value `.template` + /// - symboldRenderingMode: The symbol rendering mode to use + /// - foregroundColor: The foreground color to use when displaying this view. Default value `.accentColor` + @available(iOS 15.0, macOS 12.0, *) + init( + systemName: String, + renderingMode: Image.TemplateRenderingMode? = .template, + symboldRenderingMode: SymbolRenderingMode?, + foregroundColor: Color? = .accentColor + ) { + self.init { + Image( + systemName: systemName + ) + .renderingMode(renderingMode) + .symbolRenderingMode(symboldRenderingMode) + .font(.title) + .imageScale(.large) + .foregroundColor(foregroundColor) + } + } + +} diff --git a/Sources/Models/WhatsNew+Feature.swift b/Sources/Models/WhatsNew+Feature.swift new file mode 100644 index 0000000..fb4a963 --- /dev/null +++ b/Sources/Models/WhatsNew+Feature.swift @@ -0,0 +1,73 @@ +import SwiftUI + +// MARK: - WhatsNew+Feature + +public extension WhatsNew { + + /// A WhatsNew Feature + struct Feature { + + // MARK: Properties + + /// The image + public let image: Image + + /// The title Text + public let title: Text + + /// The subtitle Text + public let subtitle: Text + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Feature` + /// - Parameters: + /// - image: The image + /// - title: The title Text + /// - subtitle: The subtitle Text + public init( + image: Image, + title: Text, + subtitle: Text + ) { + self.image = image + self.title = title + self.subtitle = subtitle + } + + } + +} + +// MARK: - Feature+Equatable + +extension WhatsNew.Feature: Equatable { + + /// Returns a Boolean value indicating whether two values are equal. + /// - Parameters: + /// - lhs: A value to compare. + /// - rhs: Another value to compare. + public static func == ( + lhs: Self, + rhs: Self + ) -> Bool { + lhs.title == rhs.title + && lhs.subtitle == rhs.subtitle + } + +} + +// MARK: - Feature+Hashable + +extension WhatsNew.Feature: Hashable { + + /// Hashes the essential components of this value by feeding them into the given hasher. + /// - Parameter hasher: The hasher to use when combining the components of this instance. + public func hash( + into hasher: inout Hasher + ) { + hasher.combine(self.title) + hasher.combine(self.subtitle) + } + +} diff --git a/Sources/Models/WhatsNew+HapticFeedback.swift b/Sources/Models/WhatsNew+HapticFeedback.swift new file mode 100644 index 0000000..3ee87e8 --- /dev/null +++ b/Sources/Models/WhatsNew+HapticFeedback.swift @@ -0,0 +1,52 @@ +import Foundation +#if os(iOS) +import UIKit +#endif + +// MARK: - WhatsNew+HapticFeedback + +public extension WhatsNew { + + /// The WhatsNew HapticFeedback + enum HapticFeedback: Hashable { + #if os(iOS) + /// Impact HapticFeedback + case impact( + style: UIImpactFeedbackGenerator.FeedbackStyle? = nil, + intensity: CGFloat? = nil + ) + /// Selection HapticFeedback + case selection + /// Notification HapticFeedback + case notification( + UINotificationFeedbackGenerator.FeedbackType = .success + ) + #endif + } + +} + +// MARK: - Call-as-Function + +public extension WhatsNew.HapticFeedback { + + /// Call HapticFeedback as function to execute the HapticFeedback + func callAsFunction() { + #if os(iOS) + switch self { + case .impact(let style, let intensity): + let feedbackGenerator = style.flatMap(UIImpactFeedbackGenerator.init) ?? .init() + if let intensity = intensity { + feedbackGenerator.impactOccurred(intensity: intensity) + } else { + feedbackGenerator.impactOccurred() + } + case .selection: + UISelectionFeedbackGenerator().selectionChanged() + case .notification(let type): + UINotificationFeedbackGenerator().notificationOccurred(type) + } + #endif + } + +} diff --git a/Sources/Models/WhatsNew+Item.swift b/Sources/Models/WhatsNew+Item.swift deleted file mode 100644 index 0eeee39..0000000 --- a/Sources/Models/WhatsNew+Item.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// WhatsNew+Item.swift -// WhatsNewKit -// -// Created by Sven Tiigi on 19.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - WhatsNew.Item - -public extension WhatsNew { - - /// The Item - struct Item: Equatable, Hashable { - - // MARK: Properties - - /// The Title - public let title: String - - /// The Subtitle - public let subtitle: String - - /// The Image - public let image: UIImage? - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - title: The Title - /// - subtitle: The Subtitle - /// - image: The Image - public init( - title: String, - subtitle: String, - image: UIImage? - ) { - self.title = title - self.subtitle = subtitle - self.image = image - } - - } - -} - -// MARK: - Codable - -extension WhatsNew.Item: Codable { - - /// The CodingKeys - private enum CodingKeys: CodingKey { - /// Title - case title - /// Subtitle - case subtitle - /// Image - case image - } - - /// Initializer with Decoder - /// - /// - Parameter decoder: The Decoder - /// - Throws: If decoding fails - public init(from decoder: Decoder) throws { - // Initialize container keyed by CodingKeys - let container = try decoder.container(keyedBy: CodingKeys.self) - // Decode Title - self.title = try container.decode(String.self, forKey: .title) - // Decode Subtitle - self.subtitle = try container.decode(String.self, forKey: .subtitle) - // Check if Base64 Image String is available - if let base64 = try container.decodeIfPresent(String.self, forKey: .image) { - // Decode Base64 to Image - self.image = Data(base64Encoded: base64, options: .ignoreUnknownCharacters).flatMap(UIImage.init) - } else { - // No image base64 representation available - self.image = nil - } - } - - /// Encode - /// - /// - Parameter encoder: The Encoder - /// - Throws: If encoding fails - public func encode(to encoder: Encoder) throws { - // Initialize Container keyed by CodingKeys - var container = encoder.container(keyedBy: CodingKeys.self) - // Encode Title - try container.encode(self.title, forKey: .title) - // Encode Subtitle - try container.encode(self.subtitle, forKey: .subtitle) - // Try to Encode Image as Base64 string - if let image = self.image, let data = image.pngData() { - let base64 = data.base64EncodedString(options: .lineLength64Characters) - try container.encode(base64, forKey: .image) - } - } - -} diff --git a/Sources/Models/WhatsNew+Layout.swift b/Sources/Models/WhatsNew+Layout.swift new file mode 100644 index 0000000..3c01e2b --- /dev/null +++ b/Sources/Models/WhatsNew+Layout.swift @@ -0,0 +1,114 @@ +import SwiftUI + +// MARK: - WhatsNew+Layout + +public extension WhatsNew { + + /// The WhatsNew Layout + struct Layout { + + // MARK: Properties + + /// A Boolean value if the scroll view indicator should be visible + public var showsScrollViewIndicators: Bool + + /// The scroll view bottom content inset + public var scrollViewBottomContentInset: CGFloat + + /// The content spacing + public var contentSpacing: CGFloat + + /// The content padding + public var contentPadding: EdgeInsets + + /// The feature list spacing + public var featureListSpacing: CGFloat + + /// The feature list padding + public var featureListPadding: EdgeInsets + + /// The feature image width + public var featureImageWidth: CGFloat + + /// The feature horizontal spacing + public var featureHorizontalSpacing: CGFloat + + /// The feature vertical spacing + public var featureVerticalSpacing: CGFloat + + /// The footer action spacing + public var footerActionSpacing: CGFloat + + /// The corner radius of the primary action button + public var footerPrimaryActionButtonCornerRadius: CGFloat + + /// The footer visual effect view padding + public var footerVisualEffectViewPadding: EdgeInsets + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Layout` + /// - Parameters: + /// - showsScrollViewIndicators: A Boolean value if the scroll view indicator should be visible. Default value `false` + /// - scrollViewBottomContentInset: The scroll view bottom content inset. Default value `150` + /// - contentSpacing: The content spacing. Default value `60` + /// - contentPadding: The content padding. Default value `top: 65` + /// - featureListSpacing: The feature list spacing. Default value `25` + /// - featureListPadding: The feature list padding. Default value `leading: 15` + /// - featureImageWidth: The feature image width. Default value `40` + /// - featureHorizontalSpacing: The feature horizontal spacing. Default value `15` + /// - featureVerticalSpacing: The feature vertical spacing. Default value `2` + /// - footerActionSpacing: The footer action spacing. Default value `15` + /// - footerPrimaryActionButtonCornerRadius: The corner radius of the primary action button. Default value `14` + /// - footerVisualEffectViewPadding: The footer visual effect view padding. Default value `top: -10` + public init( + showsScrollViewIndicators: Bool = false, + scrollViewBottomContentInset: CGFloat = 150, + contentSpacing: CGFloat = 60, + contentPadding: EdgeInsets = .init(top: 65, leading: 0, bottom: 0, trailing: 0), + featureListSpacing: CGFloat = 25, + featureListPadding: EdgeInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 0), + featureImageWidth: CGFloat = 40, + featureHorizontalSpacing: CGFloat = 15, + featureVerticalSpacing: CGFloat = 2, + footerActionSpacing: CGFloat = 15, + footerPrimaryActionButtonCornerRadius: CGFloat = 14, + footerVisualEffectViewPadding: EdgeInsets = .init(top: -10, leading: 0, bottom: 0, trailing: 0) + ) { + self.showsScrollViewIndicators = showsScrollViewIndicators + self.scrollViewBottomContentInset = scrollViewBottomContentInset + self.contentSpacing = contentSpacing + self.contentPadding = contentPadding + self.featureListSpacing = featureListSpacing + self.featureListPadding = featureListPadding + self.featureImageWidth = featureImageWidth + self.featureHorizontalSpacing = featureHorizontalSpacing + self.featureVerticalSpacing = featureVerticalSpacing + self.footerActionSpacing = footerActionSpacing + self.footerPrimaryActionButtonCornerRadius = footerPrimaryActionButtonCornerRadius + self.footerVisualEffectViewPadding = footerVisualEffectViewPadding + } + + } + +} + +// MARK: - Layout+default + +public extension WhatsNew.Layout { + + /// The mutable default Layout + static var `default` = Self() + +} + +// MARK: - Layout+reset + +public extension WhatsNew.Layout { + + /// Reset the Layout to default values + mutating func reset() { + self = .init() + } + +} diff --git a/Sources/Models/WhatsNew+PrimaryAction.swift b/Sources/Models/WhatsNew+PrimaryAction.swift new file mode 100644 index 0000000..812ff64 --- /dev/null +++ b/Sources/Models/WhatsNew+PrimaryAction.swift @@ -0,0 +1,52 @@ +import SwiftUI + +// MARK: - WhatsNew+PrimaryAction + +public extension WhatsNew { + + /// The WhatsNew PrimaryAction + struct PrimaryAction { + + // MARK: Properties + + /// The title Text + public let title: Text + + /// The background color + public let backgroundColor: Color + + /// The foreground color + public let foregroundColor: Color + + /// The optional HapticFeedback + public let hapticFeedback: HapticFeedback? + + /// The optional on dismiss closure + public let onDismiss: (() -> Void)? + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.PrimaryAction` + /// - Parameters: + /// - title: The title Text. Default value `Continue` + /// - backgroundColor: The background color. Default value `.accentColor` + /// - foregroundColor: The foreground color. Default value `.white` + /// - hapticFeedback: The optional HapticFeedback. Default value `nil` + /// - onDismiss: The optional on dismiss closure. Default value `nil` + public init( + title: Text = "Continue", + backgroundColor: Color = .accentColor, + foregroundColor: Color = .white, + hapticFeedback: HapticFeedback? = nil, + onDismiss: (() -> Void)? = nil + ) { + self.title = title + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.hapticFeedback = hapticFeedback + self.onDismiss = onDismiss + } + + } + +} diff --git a/Sources/Models/WhatsNew+SecondaryAction+Action.swift b/Sources/Models/WhatsNew+SecondaryAction+Action.swift new file mode 100644 index 0000000..75d0e60 --- /dev/null +++ b/Sources/Models/WhatsNew+SecondaryAction+Action.swift @@ -0,0 +1,92 @@ +import SwiftUI + +// MARK: - SecondaryAction+Action + +public extension WhatsNew.SecondaryAction { + + /// A WhatsNew Secondary Action + enum Action { + /// Present View + case present(AnyView) + /// Custom Action + case custom((Binding) -> Void) + } + +} + +// MARK: - Action+present + +public extension WhatsNew.SecondaryAction.Action { + + /// Present View on WhatsNewView + /// - Parameters: + /// - content: The ViewBuilder closure that produces the Content View + static func present( + @ViewBuilder + _ content: () -> Content + ) -> Self { + .present(.init(content())) + } + +} + +// MARK: - Action+dismiss + +public extension WhatsNew.SecondaryAction.Action { + + /// Dismiss WhatsNewView + static let dismiss: Self = .custom { presentationMode in + presentationMode.wrappedValue.dismiss() + } + +} + +// MARK: - Action+openURL + +public extension WhatsNew.SecondaryAction.Action { + + /// Open a URL + /// - Parameters: + /// - url: The URL that should be opened + /// - application: The UIApplication used to open the URL. Default value `.shared` + static func openURL( + _ url: URL? + ) -> Self { + .custom { _ in + // Verify URL is available + guard let url = url else { + // Otherwise return out of function + return + } + // Open URL + #if os(macOS) + NSWorkspace.shared.open( + url + ) + #else + UIApplication.shared.open( + url, + options: .init() + ) + #endif + } + } + +} + +// MARK: - Action+PresentedView + +extension WhatsNew.SecondaryAction.Action { + + /// The WhatsNew Secondary Action PresentedView + struct PresentedView: Identifiable { + + /// The identifier + var id: UUID = .init() + + /// The View + let view: AnyView + + } + +} diff --git a/Sources/Models/WhatsNew+SecondaryAction.swift b/Sources/Models/WhatsNew+SecondaryAction.swift new file mode 100644 index 0000000..02673c9 --- /dev/null +++ b/Sources/Models/WhatsNew+SecondaryAction.swift @@ -0,0 +1,46 @@ +import SwiftUI + +// MARK: - WhatsNew+SecondaryAction + +public extension WhatsNew { + + /// The WhatsNew SecondaryAction + struct SecondaryAction { + + // MARK: Properties + + /// The title Text + public let title: Text + + /// The foreground color + public let foregroundColor: Color + + /// The optional HapticFeedback + public let hapticFeedback: HapticFeedback? + + /// The Action + public let action: Action + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.PrimaryAction` + /// - Parameters: + /// - title: The title Text + /// - foregroundColor: The foreground color. Default value `.accentColor` + /// - hapticFeedback: The optional HapticFeedback. Default value `nil` + /// - action: The Action + public init( + title: Text, + foregroundColor: Color = .accentColor, + hapticFeedback: HapticFeedback? = nil, + action: Action + ) { + self.title = title + self.foregroundColor = foregroundColor + self.hapticFeedback = hapticFeedback + self.action = action + } + + } + +} diff --git a/Sources/Models/WhatsNew+Text.swift b/Sources/Models/WhatsNew+Text.swift new file mode 100644 index 0000000..7d5fc54 --- /dev/null +++ b/Sources/Models/WhatsNew+Text.swift @@ -0,0 +1,64 @@ +import SwiftUI + +// MARK: - WhatsNew+Text + +public extension WhatsNew { + + /// A WhatsNew Text + struct Text: Hashable { + + // MARK: Properties + + /// The NSAttributedString + public let attributedString: NSAttributedString + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Text` from a given String + /// - Parameter string: The String + public init( + _ string: String + ) { + self.attributedString = .init(string: string) + } + + } + +} + +// MARK: - AttributedString Initializer + +@available(iOS 15.0, macOS 12.0, *) +public extension WhatsNew.Text { + + /// Creates a new instance of `WhatsNew.Text` from a given NSAttributedString + /// - Parameter attributedString: The NSAttributedString + init( + _ attributedString: NSAttributedString + ) { + self.attributedString = attributedString + } + + /// Creates a new instance of `WhatsNew.Text` from a given AttributedString + /// - Parameter attributedString: The AttributedString + init( + _ attributedString: AttributedString + ) { + self.attributedString = .init(attributedString) + } + +} + +// MARK: - ExpressibleByStringLiteral + +extension WhatsNew.Text: ExpressibleByStringLiteral { + + /// Creates a new instance of `WhatsNew.Text` from a given String literal + /// - Parameter value: The String literal + public init( + stringLiteral value: String + ) { + self.init(value) + } + +} diff --git a/Sources/Models/WhatsNew+Title.swift b/Sources/Models/WhatsNew+Title.swift new file mode 100644 index 0000000..60520d5 --- /dev/null +++ b/Sources/Models/WhatsNew+Title.swift @@ -0,0 +1,50 @@ +import SwiftUI + +// MARK: - WhatsNew+Title + +public extension WhatsNew { + + /// The WhatsNew Title + struct Title: Hashable { + + // MARK: Properties + + /// The title Text + public let text: Text + + /// The foreground color + public let foregroundColor: Color + + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Title` + /// - Parameters: + /// - text: The title Text + /// - foregroundColor: The foreground color. Default value `.primary` + public init( + text: Text, + foregroundColor: Color = .primary + ) { + self.text = text + self.foregroundColor = foregroundColor + } + + } + +} + +// MARK: - ExpressibleByStringLiteral + +extension WhatsNew.Title: ExpressibleByStringLiteral { + + /// Creates a new instance of `WhatsNew.Title` + /// - Parameter value: The String literal value + public init( + stringLiteral value: String + ) { + self.init( + text: .init(value) + ) + } + +} diff --git a/Sources/Models/WhatsNew+Version.swift b/Sources/Models/WhatsNew+Version.swift index ce61051..6fee254 100644 --- a/Sources/Models/WhatsNew+Version.swift +++ b/Sources/Models/WhatsNew+Version.swift @@ -1,19 +1,13 @@ -// -// WhatsNew+Version.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 22.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - import Foundation -// MARK: - Version +// MARK: - WhatsNew+Version public extension WhatsNew { - /// The Version - struct Version: Codable, Equatable, Hashable { + /// A WhatsNew Version + struct Version: Hashable { + + // MARK: Properties /// The major version public let major: Int @@ -24,8 +18,9 @@ public extension WhatsNew { /// The patch version public let patch: Int - /// Default initializer - /// + // MARK: Initializer + + /// Creates a new instance of `WhatsNew.Version` /// - Parameters: /// - major: The major version /// - minor: The minor version @@ -50,15 +45,14 @@ extension WhatsNew.Version: Comparable { /// Returns a Boolean value indicating whether the value of the first /// argument is less than that of the second argument. - /// /// - Parameters: /// - lhs: A value to compare. /// - rhs: Another value to compare. public static func < ( - lhs: WhatsNew.Version, - rhs: WhatsNew.Version + lhs: Self, + rhs: Self ) -> Bool { - return lhs.description.compare(rhs.description, options: .numeric) == .orderedAscending + lhs.description.compare(rhs.description, options: .numeric) == .orderedAscending } } @@ -69,7 +63,13 @@ extension WhatsNew.Version: CustomStringConvertible { /// A textual representation of this instance. public var description: String { - return "\(self.major).\(self.minor).\(self.patch)" + [ + self.major, + self.minor, + self.patch + ] + .map(String.init) + .joined(separator: ".") } } @@ -79,13 +79,14 @@ extension WhatsNew.Version: CustomStringConvertible { extension WhatsNew.Version: ExpressibleByStringLiteral { /// Creates an instance initialized to the given string value. - /// /// - Parameter value: The value of the new instance. - public init(stringLiteral value: String) { - let values = value.components(separatedBy: ".").compactMap(Int.init) - self.major = values.indices.contains(0) ? values[0] : 0 - self.minor = values.indices.contains(1) ? values[1] : 0 - self.patch = values.indices.contains(2) ? values[2] : 0 + public init( + stringLiteral value: String + ) { + let components = value.components(separatedBy: ".").compactMap(Int.init) + self.major = components.indices.contains(0) ? components[0] : 0 + self.minor = components.indices.contains(1) ? components[1] : 0 + self.patch = components.indices.contains(2) ? components[2] : 0 } } @@ -94,19 +95,19 @@ extension WhatsNew.Version: ExpressibleByStringLiteral { public extension WhatsNew.Version { - /// Retrieve WhatsNew.Version based on current Version String in Bundle - /// - /// - Parameter bundle: The Bundle + /// Retrieve current WhatsNew Version based on the current Version String in the Bundle + /// - Parameter bundle: The Bundle. Default value `.main` /// - Returns: WhatsNew.Version static func current( - inBundle bundle: Bundle = .main + in bundle: Bundle = .main ) -> WhatsNew.Version { // Retrieve Bundle short Version String let shortVersionString = bundle.infoDictionary?["CFBundleShortVersionString"] as? String // Return initialized Version via String Literal - return .init(stringLiteral: - shortVersionString ?? "" + return .init( + stringLiteral: shortVersionString ?? "" ) } } + diff --git a/Sources/Models/WhatsNew.swift b/Sources/Models/WhatsNew.swift index 1d03426..0a8bc75 100644 --- a/Sources/Models/WhatsNew.swift +++ b/Sources/Models/WhatsNew.swift @@ -1,117 +1,59 @@ -// -// WhatsNew.swift -// WhatsNewKit -// -// Created by Sven Tiigi on 19.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import UIKit +import Foundation // MARK: - WhatsNew -/** - The `WhatsNew` struct to declare your new app features. - Read more on: [https://github.com/SvenTiigi/WhatsNewKit](https://github.com/SvenTiigi/WhatsNewKit) - - In default the `version` property will be initialized by reading out the current app version from the main bundle. - - # Example: - ``` - import WhatsNewKit - - // WhatsNew for your current app version - let whatsNew = WhatsNew( - // The Title - title: "WhatsNewKit", - // The features you want to showcase - items: [ - WhatsNew.Item( - title: "Installation", - subtitle: "You can install WhatsNewKit via CocoaPods or Carthage", - image: UIImage(named: "installation") - ), - WhatsNew.Item( - title: "Open Source", - subtitle: "Contributions are very welcome 👨‍💻", - image: UIImage(named: "openSource") - ) - ] - ) - - // WhatsNew for a specific app version - let whatsNew = WhatsNew( - // The Title - title: "WhatsNewKit", - // The Version - version: "1.0.1", - // The features you want to showcase - items: [...] - ) - ``` - - # Note: - See the `WhatsNewViewController` to present your new app features. - */ -public struct WhatsNew: Codable, Equatable, Hashable { +/// A WhatsNew object +public struct WhatsNew { // MARK: Properties /// The Version public let version: Version - /// The title - public let title: String + /// The Title + public let title: Title + + /// The Features + public let features: [Feature] - /// The items - public let items: [Item] + /// The PrimaryAction + public let primaryAction: PrimaryAction + + /// The optional SecondaryAction + public let secondaryAction: SecondaryAction? // MARK: Initializer - /// Default initializer - /// + /// Creates a new instance of `WhatsNew` /// - Parameters: - /// - version: The Version. Default value `current` + /// - version: The Version. Default value `.current()` /// - title: The Title - /// - items: The Items + /// - items: The Features + /// - primaryAction: The PrimaryAction. Default value `.init()` + /// - secondaryAction: The optional SecondaryAction. Default value `nil` public init( - version: Version = .current(inBundle: .main), - title: String, - items: [Item] + version: Version = .current(), + title: Title, + features: [Feature], + primaryAction: PrimaryAction = .init(), + secondaryAction: SecondaryAction? = nil ) { self.version = version self.title = title - self.items = items + self.features = features + self.primaryAction = primaryAction + self.secondaryAction = secondaryAction } } -// MARK: - Sequence+get +// MARK: - Identifiable -public extension Sequence where Element == WhatsNew { - - /// Retrieve WhatsNew element based on given Version - /// - /// - Parameter version: The Version - /// - Returns: The first matching WhatsNew element - func get(byVersion version: WhatsNew.Version) -> WhatsNew? { - // First where Version is matching - return self.first(where: { - $0.version == version - }) - } +extension WhatsNew: Identifiable { - /// Retrieve WhatsNew element based on current Version of Bundle - /// - /// - Parameter bundle: The Bundle - /// - Returns: The first matching WhatsNew element - func get(byBundle bundle: Bundle = .main) -> WhatsNew? { - // Initialize current Version based on bundle - let currentVersion = WhatsNew.Version.current(inBundle: bundle) - // Return WhatsView by version - return self.get( - byVersion: currentVersion - ) + /// The stable identity of the entity associated with this instance. + public var id: Version { + self.version } } diff --git a/Sources/Store/InMemoryWhatsNewVersionStore.swift b/Sources/Store/InMemoryWhatsNewVersionStore.swift index 0c6124b..29e4ff9 100644 --- a/Sources/Store/InMemoryWhatsNewVersionStore.swift +++ b/Sources/Store/InMemoryWhatsNewVersionStore.swift @@ -1,11 +1,3 @@ -// -// InMemoryWhatsNewVersionStore.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 24.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - import Foundation // MARK: - InMemoryWhatsNewVersionStore @@ -13,31 +5,34 @@ import Foundation /// The InMemoryWhatsNewVersionStore public final class InMemoryWhatsNewVersionStore { + // MARK: Static-Properties + + /// The shared `InMemoryWhatsNewVersionStore` instance + public static let shared = InMemoryWhatsNewVersionStore() + + // MARK: Properties + /// The Versions public var versions: [WhatsNew.Version] - /// Default initializer + // MARK: Initializer + + /// Creates a new instance of `InMemoryWhatsNewVersionStore` public init() { - // Initialize Version Array self.versions = .init() } - /// Clear all stored Versions - public func clearVersions() { - self.versions.removeAll() - } - } // MARK: - WriteableWhatsNewVersionStore extension InMemoryWhatsNewVersionStore: WriteableWhatsNewVersionStore { - /// Set Version - /// - /// - Parameter version: The Version - public func set(version: WhatsNew.Version) { - // Append Version + /// Save presented WhatsNew Version + /// - Parameter version: The presented WhatsNew Version that should be saved + public func save( + presentedVersion version: WhatsNew.Version + ) { self.versions.append(version) } @@ -47,13 +42,20 @@ extension InMemoryWhatsNewVersionStore: WriteableWhatsNewVersionStore { extension InMemoryWhatsNewVersionStore: ReadableWhatsNewVersionStore { - /// Has Version - /// - /// - Parameter version: The Version - /// - Returns: Bool if Version has been presented - public func has(version: WhatsNew.Version) -> Bool { - // Return if versions is contained in versions - return self.versions.contains(version) + /// The WhatsNew Versions that have been already been presented + public var presentedVersions: [WhatsNew.Version] { + self.versions + } + +} + +// MARK: - Remove all + +public extension InMemoryWhatsNewVersionStore { + + /// Remove all presented WhatsNew Versions + func removeAll() { + self.versions.removeAll() } } diff --git a/Sources/Store/KeyValueWhatsNewVersionStore.swift b/Sources/Store/KeyValueWhatsNewVersionStore.swift deleted file mode 100644 index 7b6b99b..0000000 --- a/Sources/Store/KeyValueWhatsNewVersionStore.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// KeyValueWhatsNewVersionStore.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 27.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import Foundation - -// MARK: - KeyValueable - -/// The KeyValueable Protocol -public protocol KeyValueable { - - /// Set value for key - /// - /// - Parameters: - /// - value: The value - /// - key: The key - func set(_ value: Any?, forKey key: String) - - /// Retrieve object for key - /// - /// - Parameter forKey: The key - /// - Returns: The corresponding object - func object(forKey: String) -> Any? - -} - -// MARK: - KeyValueable UserDefaults - -extension UserDefaults: KeyValueable {} - -// MARK: - NSUbiquitousKeyValueStore UserDefaults - -extension NSUbiquitousKeyValueStore: KeyValueable {} - -// MARK: - KeyValueWhatsNewVersionStore - -/// The KeyValueWhatsNewVersionStore -public struct KeyValueWhatsNewVersionStore { - - /// The KeyValueable - private let keyValueable: KeyValueable - - /// The prefix identifier - private let prefixIdentifier: String - - // MARK: Initializer - - /// Default initializer - /// - /// - Parameters: - /// - keyValueable: The KeyValueable Object. Default value `UserDefaults.standard` - /// - prefixIdentifier: The prefix identifier. Default value `de.tiigi.whatsnewkit.` - public init( - keyValueable: KeyValueable = UserDefaults.standard, - prefixIdentifier: String = "de.tiigi.whatsnewkit" - ) { - self.keyValueable = keyValueable - self.prefixIdentifier = prefixIdentifier - } - - // MARK: Private API - - /// Retrieve Key for Version - /// - /// - Parameter version: The Version - /// - Returns: String key concatenated with prefix identifier - private func key(forVersion version: WhatsNew.Version) -> String { - return "\(self.prefixIdentifier)\(!self.prefixIdentifier.isEmpty ? "." : "")\(version)" - } - -} - -// MARK: - WriteableWhatsNewVersionStore - -extension KeyValueWhatsNewVersionStore: WriteableWhatsNewVersionStore { - - /// Set Version - /// - /// - Parameter version: The Version - public func set(version: WhatsNew.Version) { - // Set Version String representation - self.keyValueable.set( - version.description, - forKey: self.key(forVersion: version) - ) - } - -} - -// MARK: - ReadableWhatsNewVersionStore - -extension KeyValueWhatsNewVersionStore: ReadableWhatsNewVersionStore { - - /// Has Version - /// - /// - Parameter version: The Version - /// - Returns: Bool if Version has been presented - public func has(version: WhatsNew.Version) -> Bool { - // Retrieve object for key as String - let versionObjectString = self.keyValueable.object(forKey: self.key(forVersion: version)) as? String - // Return string comparison result - return versionObjectString == version.description - } - -} diff --git a/Sources/Store/NSUbiquitousKeyValueWhatsNewVersionStore.swift b/Sources/Store/NSUbiquitousKeyValueWhatsNewVersionStore.swift new file mode 100644 index 0000000..a0dedc3 --- /dev/null +++ b/Sources/Store/NSUbiquitousKeyValueWhatsNewVersionStore.swift @@ -0,0 +1,72 @@ +import Foundation + +/// The CloudKitWhatsNewVersionStore typealias representing a NSUbiquitousKeyValueWhatsNewVersionStore +public typealias CloudKitWhatsNewVersionStore = NSUbiquitousKeyValueWhatsNewVersionStore + +// MARK: - NSUbiquitousKeyValueWhatsNewVersionStore + +/// A NSUbiquitousKeyValue WhatsNewVersionStore +public struct NSUbiquitousKeyValueWhatsNewVersionStore { + + // MARK: Properties + + /// The NSUbiquitousKeyValueStore + private let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore + + // MARK: Initializer + + /// Creates a new instance of `NSUbiquitousKeyValueWhatsNewVersionStore` + /// - Parameters: + /// - ubiquitousKeyValueStore: The NSUbiquitousKeyValueWhatsNewVersionStore. Default value `.default` + public init( + ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = .default + ) { + self.ubiquitousKeyValueStore = ubiquitousKeyValueStore + } + +} + +// MARK: - WriteableWhatsNewVersionStore + +extension NSUbiquitousKeyValueWhatsNewVersionStore: WriteableWhatsNewVersionStore { + + /// Save presented WhatsNew Version + /// - Parameter version: The presented WhatsNew Version that should be saved + public func save( + presentedVersion version: WhatsNew.Version + ) { + self.ubiquitousKeyValueStore.set( + version.description, + forKey: version.key + ) + } + +} + +// MARK: - ReadableWhatsNewVersionStore + +extension NSUbiquitousKeyValueWhatsNewVersionStore: ReadableWhatsNewVersionStore { + + /// The WhatsNew Versions that have been already been presented + public var presentedVersions: [WhatsNew.Version] { + self.ubiquitousKeyValueStore + .dictionaryRepresentation + .filter { $0.key.starts(with: WhatsNew.Version.keyPrefix) } + .compactMap { $0.value as? String } + .map(WhatsNew.Version.init) + } + +} + +// MARK: - Remove all + +public extension NSUbiquitousKeyValueWhatsNewVersionStore { + + /// Remove all presented Versions + func removeAll() { + self.presentedVersions + .map(\.key) + .forEach(self.ubiquitousKeyValueStore.removeObject) + } + +} diff --git a/Sources/Store/UserDefaultsWhatsNewVersionStore.swift b/Sources/Store/UserDefaultsWhatsNewVersionStore.swift new file mode 100644 index 0000000..9603cce --- /dev/null +++ b/Sources/Store/UserDefaultsWhatsNewVersionStore.swift @@ -0,0 +1,69 @@ +import Foundation + +// MARK: - UserDefaultsWhatsNewVersionStore + +/// A UserDefaults WhatsNewVersionStore +public struct UserDefaultsWhatsNewVersionStore { + + // MARK: Properties + + /// The UserDefaults + private let userDefaults: UserDefaults + + // MARK: Initializer + + /// Creates a new instance of `UserDefaultsWhatsNewVersionStore` + /// - Parameters: + /// - userDefaults: The UserDefaults. Default value `.standard` + public init( + userDefaults: UserDefaults = .standard + ) { + self.userDefaults = userDefaults + } + +} + +// MARK: - WriteableWhatsNewVersionStore + +extension UserDefaultsWhatsNewVersionStore: WriteableWhatsNewVersionStore { + + /// Save presented WhatsNew Version + /// - Parameter version: The presented WhatsNew Version that should be saved + public func save( + presentedVersion version: WhatsNew.Version + ) { + self.userDefaults.set( + version.description, + forKey: version.key + ) + } + +} + +// MARK: - ReadableWhatsNewVersionStore + +extension UserDefaultsWhatsNewVersionStore: ReadableWhatsNewVersionStore { + + /// The WhatsNew Versions that have been already been presented + public var presentedVersions: [WhatsNew.Version] { + self.userDefaults + .dictionaryRepresentation() + .filter { $0.key.starts(with: WhatsNew.Version.keyPrefix) } + .compactMap { $0.value as? String } + .map(WhatsNew.Version.init) + } + +} + +// MARK: - Remove all + +public extension UserDefaultsWhatsNewVersionStore { + + /// Remove all presented Versions + func removeAll() { + self.presentedVersions + .map(\.key) + .forEach(self.userDefaults.removeObject) + } + +} diff --git a/Sources/Store/WhatsNewVersionStore.swift b/Sources/Store/WhatsNewVersionStore.swift index 36c4200..a322e30 100644 --- a/Sources/Store/WhatsNewVersionStore.swift +++ b/Sources/Store/WhatsNewVersionStore.swift @@ -1,39 +1,53 @@ -// -// WhatsNewVersionStore.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 21.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - import Foundation // MARK: - WhatsNewVersionStore -/// The WhatsNewVersionStore typealias +/// A WhatsNewVersionStore public typealias WhatsNewVersionStore = WriteableWhatsNewVersionStore & ReadableWhatsNewVersionStore // MARK: - WriteableWhatsNewVersionStore -/// The WriteableWhatsNewVersionStore +/// A Writeable WhatsNewVersionStore public protocol WriteableWhatsNewVersionStore { - /// Set Version - /// - /// - Parameter version: The Version - func set(version: WhatsNew.Version) + /// Save presented WhatsNew Version + /// - Parameter version: The presented WhatsNew Version that should be saved + func save( + presentedVersion version: WhatsNew.Version + ) } // MARK: - ReadableWhatsNewVersionStore -/// The ReadableWhatsNewVersionStore +/// A Readable WhatsNewVersionStore public protocol ReadableWhatsNewVersionStore { - /// Has Version - /// - /// - Parameter version: The Version - /// - Returns: Bool if Version has been presented - func has(version: WhatsNew.Version) -> Bool + /// The WhatsNew Versions that have been already been presented + var presentedVersions: [WhatsNew.Version] { get } + +} + +// MARK: - ReadableWhatsNewVersionStore+hasPresented + +public extension ReadableWhatsNewVersionStore { + + /// Retrieve a bool value if a given WhatsNew Version has already been presented + /// - Parameter whatsNew: The WhatsNew Version to verify + /// - Returns: A Bool value if the given WhatsNew Version has already been preseted + func hasPresented( + _ version: WhatsNew.Version + ) -> Bool { + self.presentedVersions.contains(version) + } + + /// Retrieve a bool value if a given WhatsNew has already been presented + /// - Parameter whatsNew: The WhatsNew to verify + /// - Returns: A Bool value if the given WhatsNew has already been preseted + func hasPresented( + _ whatsNew: WhatsNew + ) -> Bool { + self.hasPresented(whatsNew.version) + } } diff --git a/Sources/Theme/WhatsNewViewController+Theme.swift b/Sources/Theme/WhatsNewViewController+Theme.swift deleted file mode 100644 index a2f97c9..0000000 --- a/Sources/Theme/WhatsNewViewController+Theme.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// WhatsNewViewController+Theme.swift -// WhatsNewKit-Example -// -// Created by Sven Tiigi on 02.11.19. -// Copyright © 2019 WhatsNewKit. All rights reserved. -// - -import UIKit - -// MARK: - Theme - -public extension WhatsNewViewController { - - /// The Customization typealias for an inout Configuration closure - typealias Customization = (inout Configuration) -> Void - - /// The Theme - struct Theme { - - /// The Customization closure - let customization: Customization - - /// Default initializer - /// - /// - Parameter customization: The Customization - public init(customization: @escaping Customization) { - self.customization = customization - } - - } - -} - -// MARK: - Default Template Theme - -public extension WhatsNewViewController.Theme { - - /// The default Theme - static var `default`: WhatsNewViewController.Theme { - return .blue - } - -} - -// MARK: - Blue Theme - -public extension WhatsNewViewController.Theme { - - /// Default Theme - static var blue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .whatsNewKitBlue - } - } - - /// White Blue Theme (white background and blue tint color) - static var whiteBlue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .whatsNewKitBlue - } - } - - /// Dark Blue Theme (dark background and blue tint color) - static var darkBlue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .whatsNewKitBlue - } - } - -} - -// MARK: - LightBlue Theme - -public extension WhatsNewViewController.Theme { - - /// LightBlue Theme - static var lightBlue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .whatsNewKitLightBlue - } - } - - /// White LightBlue Theme (white background and light blue tint color) - static var whiteLightBlue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .whatsNewKitLightBlue - } - } - - /// Dark LightBlue Theme (dark background and light blue tint color) - static var darkLightBlue: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .whatsNewKitLightBlue - } - } - -} - -// MARK: - Orange Theme - -public extension WhatsNewViewController.Theme { - - /// Orange Theme - static var orange: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .orange - } - } - - /// White Orange Theme (white background and orange tint color) - static var whiteOrange: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .orange - } - } - - /// Dark Orange Theme (dark background and orange tint color) - static var darkOrange: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .orange - } - } - -} - -// MARK: - Purple Theme - -public extension WhatsNewViewController.Theme { - - /// Purple Theme - static var purple: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .whatsNewKitPurple - } - } - - /// White Purple Theme (white background and purple tint color) - static var whitePurple: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .whatsNewKitPurple - } - } - - /// Dark Purple Theme (dark background and purple tint color) - static var darkPurple: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .whatsNewKitPurple - } - } - -} - -// MARK: - Red Theme - -public extension WhatsNewViewController.Theme { - - /// Red Theme - static var red: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .whatsNewKitRed - } - } - - /// White Red Theme (white background and red tint color) - static var whiteRed: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .whatsNewKitRed - } - } - - /// Dark Red Theme (dark background and red tint color) - static var darkRed: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .whatsNewKitRed - } - } - -} - -// MARK: - Green Theme - -public extension WhatsNewViewController.Theme { - - /// Green Theme - static var green: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitBackground - configuration.apply(textColor: .whatsNewKitForeground) - configuration.tintColor = .whatsNewKitGreen - } - } - - /// White Green Theme (white background and green tint color) - static var whiteGreen: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitWhite - configuration.apply(textColor: .whatsNewKitBlack) - configuration.tintColor = .whatsNewKitGreen - } - } - - /// Dark Green Theme (dark background and green tint color) - static var darkGreen: WhatsNewViewController.Theme { - return .init { configuration in - configuration.backgroundColor = .whatsNewKitDark - configuration.apply(textColor: .whatsNewKitWhite) - configuration.tintColor = .whatsNewKitGreen - } - } - -} diff --git a/Sources/View/WhatsNewView+FeaturesPadding.swift b/Sources/View/WhatsNewView+FeaturesPadding.swift new file mode 100644 index 0000000..8b70ad1 --- /dev/null +++ b/Sources/View/WhatsNewView+FeaturesPadding.swift @@ -0,0 +1,51 @@ +import SwiftUI + +// MARK: - WhatsNewView+FeaturesPadding + +extension WhatsNewView { + + /// The WhatsNewView FeaturesPadding ViewModifier + struct FeaturesPadding { + + #if os(iOS) + /// The Horizontal SizeClass + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass + + /// The Vertical SizeClass + @Environment(\.verticalSizeClass) + private var verticalSizeClass + #endif + + } + +} + +// MARK: - ViewModifier + +extension WhatsNewView.FeaturesPadding: ViewModifier { + + /// Gets the current body of the caller. + /// - Parameter content: The Content + func body( + content: Content + ) -> some View { + #if os(macOS) + content.padding(.horizontal) + #else + if self.horizontalSizeClass == .regular { + content.padding( + .init( + top: 0, + leading: 100, + bottom: 0, + trailing: 100 + ) + ) + } else { + content + } + #endif + } + +} diff --git a/Sources/View/WhatsNewView+FooterPadding.swift b/Sources/View/WhatsNewView+FooterPadding.swift new file mode 100644 index 0000000..8418714 --- /dev/null +++ b/Sources/View/WhatsNewView+FooterPadding.swift @@ -0,0 +1,67 @@ +import SwiftUI + +// MARK: - WhatsNewView+FooterPadding + +extension WhatsNewView { + + /// The WhatsNewView FooterPadding ViewModifier + struct FooterPadding { + + #if os(iOS) + /// The Horizontal SizeClass + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass + + /// The Vertical SizeClass + @Environment(\.verticalSizeClass) + private var verticalSizeClass + #endif + + } + +} + +// MARK: - ViewModifier + +extension WhatsNewView.FooterPadding: ViewModifier { + + /// Gets the current body of the caller. + /// - Parameter content: The Content + func body( + content: Content + ) -> some View { + #if os(macOS) + content.padding(.bottom, 30) + #else + if self.horizontalSizeClass == .regular { + content.padding( + .init( + top: 0, + leading: 150, + bottom: 50, + trailing: 150 + ) + ) + } else if self.verticalSizeClass == .compact { + content.padding( + .init( + top: 0, + leading: 40, + bottom: 35, + trailing: 40 + ) + ) + } else { + content.padding( + .init( + top: 0, + leading: 20, + bottom: 80, + trailing: 20 + ) + ) + } + #endif + } + +} diff --git a/Sources/View/WhatsNewView+PrimaryButtonStyle.swift b/Sources/View/WhatsNewView+PrimaryButtonStyle.swift new file mode 100644 index 0000000..1b3fbd3 --- /dev/null +++ b/Sources/View/WhatsNewView+PrimaryButtonStyle.swift @@ -0,0 +1,53 @@ +import SwiftUI + +// MARK: - WhatsNewView+PrimaryButtonStyle + +extension WhatsNewView { + + /// The WhatsNewView PrimaryButtonStyle + struct PrimaryButtonStyle { + + /// The WhatsNew PrimaryAction + let primaryAction: WhatsNew.PrimaryAction + + /// The WhatsNew Layout + let layout: WhatsNew.Layout + + } + +} + +// MARK: - ButtonStyle + +extension WhatsNewView.PrimaryButtonStyle: ButtonStyle { + + /// Creates a view that represents the body of a button. + /// - Parameter configuration: The properties of the button. + func makeBody( + configuration: Configuration + ) -> some View { + Group { + #if os(iOS) + HStack { + Spacer() + configuration + .label + .font(.headline.weight(.semibold)) + .padding(.vertical) + Spacer() + } + #else + configuration + .label + .padding(.horizontal, 60) + .padding(.vertical, 8) + #endif + } + .foregroundColor(self.primaryAction.foregroundColor) + .background(self.primaryAction.backgroundColor) + .cornerRadius(self.layout.footerPrimaryActionButtonCornerRadius) + .opacity(configuration.isPressed ? 0.5 : 1) + } + +} + diff --git a/Sources/View/WhatsNewView.swift b/Sources/View/WhatsNewView.swift new file mode 100644 index 0000000..603c7ce --- /dev/null +++ b/Sources/View/WhatsNewView.swift @@ -0,0 +1,238 @@ +import SwiftUI + +// MARK: - WhatsNewView + +/// A WhatsNewView +public struct WhatsNewView { + + // MARK: Properties + + /// The WhatsNew object + private let whatsNew: WhatsNew + + /// The WhatsNewVersionStore + private let whatsNewVersionStore: WhatsNewVersionStore? + + /// The WhatsNew Layout + private let layout: WhatsNew.Layout + + /// The View that is presented by the SecondaryAction + @State + private var secondaryActionPresentedView: WhatsNew.SecondaryAction.Action.PresentedView? + + /// The PresentationMode + @Environment(\.presentationMode) + private var presentationMode + + // MARK: Initializer + + /// Creates a new instance of `WhatsNewView` + /// - Parameters: + /// - whatsNew: The WhatsNew object + /// - versionStore: The optional WhatsNewVersionStore. Default value `nil` + /// - layout: The WhatsNew Layout. Default value `.default` + public init( + whatsNew: WhatsNew, + versionStore: WhatsNewVersionStore? = nil, + layout: WhatsNew.Layout = .default + ) { + self.whatsNew = whatsNew + self.whatsNewVersionStore = versionStore + self.layout = layout + } + +} + +// MARK: - View + +extension WhatsNewView: View { + + /// The content and behavior of the view. + public var body: some View { + ZStack { + // Content ScrollView + ScrollView( + .vertical, + showsIndicators: self.layout.showsScrollViewIndicators + ) { + // Content Stack + VStack( + spacing: self.layout.contentSpacing + ) { + // Title + self.title + // Feature List + VStack( + alignment: .leading, + spacing: self.layout.featureListSpacing + ) { + // Feature + ForEach( + self.whatsNew.features, + id: \.self, + content: self.feature + ) + } + .modifier(FeaturesPadding()) + .padding(self.layout.featureListPadding) + } + .padding(.horizontal) + .padding(self.layout.contentPadding) + // ScrollView bottom content inset + Color.clear + .padding( + .bottom, + self.layout.scrollViewBottomContentInset + ) + } + #if os(iOS) + .alwaysBounceVertical(false) + #endif + // Footer + VStack { + Spacer() + self.footer + .modifier(FooterPadding()) + #if os(iOS) + .background( + UIVisualEffectView + .Representable() + .edgesIgnoringSafeArea(.horizontal) + .padding(self.layout.footerVisualEffectViewPadding) + ) + #endif + } + .edgesIgnoringSafeArea(.bottom) + } + .sheet( + item: self.$secondaryActionPresentedView, + content: { $0.view } + ) + .onDisappear { + // Save presented WhatsNew Version, if available + self.whatsNewVersionStore?.save( + presentedVersion: self.whatsNew.version + ) + } + } + +} + +// MARK: - Title + +private extension WhatsNewView { + + /// The Title View + var title: some View { + Text( + whatsNewText: self.whatsNew.title.text + ) + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + } + +} + +// MARK: - Feature + +private extension WhatsNewView { + + /// The Feature View + /// - Parameter feature: A WhatsNew Feature + func feature( + _ feature: WhatsNew.Feature + ) -> some View { + HStack( + spacing: self.layout.featureHorizontalSpacing + ) { + feature + .image + .view() + .frame(width: self.layout.featureImageWidth) + VStack( + alignment: .leading, + spacing: self.layout.featureVerticalSpacing + ) { + Text( + whatsNewText: feature.title + ) + .font(.subheadline.weight(.semibold)) + .foregroundColor(.primary) + Text( + whatsNewText: feature.subtitle + ) + .font(.subheadline) + .foregroundColor(.secondary) + } + .multilineTextAlignment(.leading) + } + } + +} + +// MARK: - Footer + +private extension WhatsNewView { + + /// The Footer View + var footer: some View { + VStack( + spacing: self.layout.footerActionSpacing + ) { + // Check if a secondary action is available + if let secondaryAction = self.whatsNew.secondaryAction { + // Secondary Action Button + Button( + action: { + // Invoke HapticFeedback, if available + secondaryAction.hapticFeedback?() + // Switch on Action + switch secondaryAction.action { + case .present(let view): + // Set secondary action presented view + self.secondaryActionPresentedView = .init(view: view) + case .custom(let action): + // Invoke action with PresentationMode + action(self.presentationMode) + } + } + ) { + Text( + whatsNewText: secondaryAction.title + ) + } + #if os(macOS) + .buttonStyle( + PlainButtonStyle() + ) + #endif + .foregroundColor(secondaryAction.foregroundColor) + } + // Primary Action Button + Button( + action: { + // Invoke HapticFeedback, if available + self.whatsNew.primaryAction.hapticFeedback?() + // Dismiss + self.presentationMode.wrappedValue.dismiss() + // Invoke on dismiss, if available + self.whatsNew.primaryAction.onDismiss?() + } + ) { + Text( + whatsNewText: self.whatsNew.primaryAction.title + ) + } + .buttonStyle( + PrimaryButtonStyle( + primaryAction: self.whatsNew.primaryAction, + layout: self.layout + ) + ) + #if os(macOS) + .keyboardShortcut(.defaultAction) + #endif + } + } + +} diff --git a/Sources/ViewController/WhatsNewViewController.swift b/Sources/ViewController/WhatsNewViewController.swift new file mode 100644 index 0000000..a2400b1 --- /dev/null +++ b/Sources/ViewController/WhatsNewViewController.swift @@ -0,0 +1,70 @@ +import SwiftUI + +// MARK: - WhatsNewHostingController + +#if os(macOS) +/// A WhatsNewHostingController +public typealias WhatsNewHostingController = NSHostingController +#else +/// A WhatsNewHostingController +public typealias WhatsNewHostingController = UIHostingController +#endif + +// MARK: - WhatsNewViewController + +/// A WhatsNew UIViewController +open class WhatsNewViewController: WhatsNewHostingController { + + /// Creates a new instance of `WhatsNewViewController` + /// - Parameters: + /// - whatsNew: The WhatsNew object + /// - layout: The WhatsNew Layout. Default value `.default` + public init( + whatsNew: WhatsNew, + layout: WhatsNew.Layout = .default + ) { + super.init( + rootView: .init( + whatsNew: whatsNew, + layout: layout + ) + ) + } + + /// Creates a new instance of `WhatsNewViewController` + /// by using the provided `WhatsNewVersionStore` to verify that the + /// version of the WhatsNew object has not already been presented to the user. + /// If the version is contained in the provided `WhatsNewVersionStore` the initializer + /// will return `nil` + /// - Parameters: + /// - whatsNew: The WhatsNew object + /// - versionStore: The WhatsNewVersionStore + /// - layout: The WhatsNew Layout. Default value `.default` + public init?( + whatsNew: WhatsNew, + versionStore: WhatsNewVersionStore, + layout: WhatsNew.Layout = .default + ) { + // Verify WhatsNew Version has not already been presented + guard !versionStore.hasPresented(whatsNew) else { + // Otherwise return nil as WhatsNew Version + // has already been presented to the user + return nil + } + super.init( + rootView: .init( + whatsNew: whatsNew, + versionStore: versionStore, + layout: layout + ) + ) + } + + /// Initializer with NSCoder is unavailable. + /// Please use `init(whatsNew:)` or `init?(whatsNew:versionStore:)` + @available(*, unavailable) + public required init?( + coder aDecoder: NSCoder + ) { nil } + +} diff --git a/Sources/WhatsNewViewController.swift b/Sources/WhatsNewViewController.swift deleted file mode 100644 index 172f037..0000000 --- a/Sources/WhatsNewViewController.swift +++ /dev/null @@ -1,521 +0,0 @@ -// -// WhatsNewViewController.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 19.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import SafariServices -import UIKit - -// MARK: - WhatsNewViewController - -/** -The `WhatsNewViewController` to easily showcase your awesome new app features. -Read more on: [https://github.com/SvenTiigi/WhatsNewKit](https://github.com/SvenTiigi/WhatsNewKit) - -Simply pass your `WhatsNew` struct to the `WhatsNewViewController` initializer -and present it. - -``` -import WhatsNewKit - -// Initialize WhatsNewViewController with WhatsNew -let whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew -) - -// Present it 🤩 -self.present(whatsNewViewController, animated: true) -``` - -# Custom Configuration - - If you wish to customize the appearance of the `WhatsNewViewController` you can - initialize it with a configured `WhatsNewViewController.Configuration`. - - ``` - // Initialize default Configuration - var configuration = WhatsNewViewController.Configuration() - - // Customize the Configuration to your needs - configuration.titleView.titleColor = .orange - // ... - - // Initialize WhatsNewViewController with custom configuration - let whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew, - configuration: configuration - ) - ``` - - # Themes - - Beside the aforementioned custom configuration you can make use of predefined themes. - - ``` - // Initialize WhatsNewViewController with a theme - let whatsNewViewController = WhatsNewViewController( - whatsNew: whatsNew, - configuration: .init(theme: .darkRed) - ) - ``` - - # WhatsNewVersionStore - - If you pass a `WhatsNewVersionStore` to the initializer will become `optional`. - The `WhatsNewViewController` will return nil during initialization if the `version` of your passed `WhatsNew` is - contained in the `WhatsNewVersionStore`. - - ``` - // Initialize WhatsNewViewController with WhatsNewVersionStore - let whatsNewViewController: WhatsNewViewController? = WhatsNewViewController( - whatsNew: whatsNew, - versionStore: myVersionStore - ) - - // Check if WhatsNewViewController is available to present it. - if let controller = whatsNewViewController { - // Present it as WhatsNewViewController is available - // after init with WhatsNewVersionStore - self.present(controller, animated: true) - } else { - // WhatsNewViewController is `nil` - // this Version has already been presented - } - ``` - - If the `version` is not contained in the `WhatsNewVersionStore` the `WhatsNewViewController` will - automatically save the presented version inside the passed `WhatsNewVersionStore` to ensure that the presentation - of your new app features only happens once. -*/ -public final class WhatsNewViewController: UIViewController { - - // MARK: Properties - - /// The UserInterfaceIdiom - static var userInterfaceIdiom = UIDevice.current.userInterfaceIdiom - - /// The preferred status bar style - public override var preferredStatusBarStyle: UIStatusBarStyle { - return self.view.backgroundColor?.isLight == true ? .default : .lightContent - } - - /// Specifies whether the view controller prefers the status bar to be hidden or shown. - public override var prefersStatusBarHidden: Bool { - return self.configuration.prefersStatusBarHidden - } - - /// The WhatsNew - public let whatsNew: WhatsNew - - /// The Configuration - var configuration: Configuration - - /// The VersionStore - var versionStore: WhatsNewVersionStore? - - /// The Constraint DisposeBag - lazy var constraintDisposeBag = DisposeBag() - - /// The TitleViewController - lazy var titleViewController = WhatsNewTitleViewController( - title: self.whatsNew.title, - configuration: self.configuration - ) - - /// The ItemsViewController - lazy var itemsViewController = WhatsNewItemsViewController( - items: self.whatsNew.items, - configuration: self.configuration - ) - - /// The ButtonViewController - lazy var buttonViewController = WhatsNewButtonViewController( - configuration: self.configuration, - onPress: { [weak self] buttonType in - // Handle Button Press - self?.handlePress(buttonType: buttonType) - } - ) - - // MARK: Initializer - - /// Designated Initializer with WhatsNew and Configuration - /// - /// - Parameters: - /// - whatsNew: The WhatsNew - /// - configuration: The Configuration. Default value `.init()` - public init( - whatsNew: WhatsNew, - configuration: Configuration = .init() - ) { - // Set WhatsNew - self.whatsNew = whatsNew - // Set Configuration - self.configuration = configuration - // Super init - super.init(nibName: nil, bundle: nil) - } - - /// Convenience optional initializer with WhatsNewVersionStore. - /// Initializer checks via WhatsNewVersionStore if Version has already been presented. - /// If a Version has been found the initializer will return nil. - /// - /// - Parameters: - /// - whatsNew: The WhatsNew - /// - configuration: The Configuration - /// - versionStore: The WhatsNewVersionStore - public convenience init?( - whatsNew: WhatsNew, - configuration: Configuration = .init(), - versionStore: WhatsNewVersionStore - ) { - // Verify VersionStore has not stored the WhatsNew Version - guard !versionStore.has(version: whatsNew.version) else { - // Return nil as Version has already been presented - return nil - } - // Self init with WhatsNew and Configuration - self.init( - whatsNew: whatsNew, - configuration: configuration - ) - // Set VersionStore - self.versionStore = versionStore - } - - /// Convenience Initializer with WhatsNew and a Theme - /// - /// - Parameters: - /// - whatsNew: The WhatsNew - /// - theme: The Theme - public convenience init( - whatsNew: WhatsNew, - theme: Theme - ) { - self.init( - whatsNew: whatsNew, - configuration: .init(theme) - ) - } - - /// Convenience Initializer with WhatsNew, Theme and WhatsNewVersionStore - /// Initializer checks via WhatsNewVersionStore if Version has already been presented. - /// If a Version has been found the initializer will return nil. - /// - /// - Parameters: - /// - whatsNew: The WhatsNew - /// - theme: The Theme - public convenience init?( - whatsNew: WhatsNew, - theme: Theme, - versionStore: WhatsNewVersionStore - ) { - self.init( - whatsNew: whatsNew, - configuration: .init(theme), - versionStore: versionStore - ) - } - - /// Initializer with Coder always returns nil - public required init?(coder aDecoder: NSCoder) { - return nil - } - - /// Deinit - deinit { - // Store WhatsNew Version - self.storeWhatsNewVersion() - } - - // MARK: View-Lifecycle - - /// View did load - public override func viewDidLoad() { - // Invoke super - super.viewDidLoad() - // Set background color - self.view.backgroundColor = self.configuration.backgroundColor - // Add Subviews - self.addSubviews() - // Make Constraints - self.makeConstraints(configuration: self.configuration) - } - - /// TraitCollection did change - /// - Parameter previousTraitCollection: The previous TraitCollection - public override func traitCollectionDidChange( - _ previousTraitCollection: UITraitCollection? - ) { - // Invoke the super implementation - super.traitCollectionDidChange(previousTraitCollection) - // Verify InterfaceIdiom is Pad - guard WhatsNewViewController.userInterfaceIdiom == .pad else { - // Otherwise return out of function - return - } - // Switch on horizontal size class - switch self.traitCollection.horizontalSizeClass { - case .compact: - // Remake Constraints with default Configuration - self.remakeConstraints(configuration: self.configuration) - default: - // Initialize mutable Configuration - var configuration = self.configuration - // Perform PadAdjustement on mutable Configuration - configuration.padAdjustment(&configuration) - // Remake Constraints with mutated Configuration - self.remakeConstraints( - configuration: configuration - ) - } - } - -} - -// MARK: - Add Subviews - -extension WhatsNewViewController { - - /// Add Subviews - func addSubviews() { - // Switch on TitleMode - switch self.configuration.titleView.titleMode { - case .fixed: - // Add TitleViewController as Child-ViewController - self.addChild(self.titleViewController) - // Fixed Mode add TitleViewController view to subview - self.view.addSubview(self.titleViewController.view) - case .scrolls: - // Add TitleViewController as Child-ViewController on ItemsViewController - self.itemsViewController.addChild(self.titleViewController) - // Scroll Mode add TitleViewController view as TableHeaderView - self.itemsViewController.tableView.tableHeaderView = { - // Retrieve TitleViewController View - let titleView = self.titleViewController.view as UIView - // Set Height - titleView.bounds.size.height = titleView.intrinsicContentSize.height - + self.configuration.titleView.insets.top - + self.configuration.titleView.insets.bottom - // Return the TitleView - return titleView - }() - } - // Invoke didMove Lifecycle on TitleViewController - self.titleViewController.didMove(toParent: self) - // Add ItemsViewController as Child-ViewController - self.addChild(self.itemsViewController) - // Add ItemsViewController View to Subview - self.view.addSubview(self.itemsViewController.view) - // Invoke didMove Lifecycle on ItemsViewController - self.itemsViewController.didMove(toParent: self) - // Add ButtonViewController as Child-ViewController - self.addChild(self.buttonViewController) - // Add ButtonViewController View to Subview - self.view.addSubview(self.buttonViewController.view) - // Invoke didMove Lifecycle on ButtonViewController - self.buttonViewController.didMove(toParent: self) - } - -} - -// MARK: - Make Constraints - -extension WhatsNewViewController { - - /// Remake Constraints - /// - Parameter configuration: The Configuration that should be used - func remakeConstraints(configuration: Configuration) { - // Dispose the Constraint DisposeBag in order to remove any previous constraints - self.constraintDisposeBag.dispose() - // Re-Make Constraints with Configuration - self.makeConstraints(configuration: configuration) - } - - /// Make Constraints - /// - Parameter configuration: The Configuration that should be used - func makeConstraints(configuration: Configuration) { - // Declare ItemsView TopAnchor - let itemsViewTopAnchor: NSLayoutYAxisAnchor - // Declare ItemsView TopAnchor Constraints - let itemsViewTopAnchorConstant: CGFloat - // Switch on TitleMode - switch configuration.titleView.titleMode { - case .fixed: - // Make Constraints on TitleViewController - self.titleViewController.view.makeConstraints( - self.titleViewController.view.topAnchor.constraint( - equalTo: self.anchor.topAnchor, - constant: configuration.titleView.insets.top - ), - self.titleViewController.view.leadingAnchor.constraint( - equalTo: self.anchor.leadingAnchor, - constant: configuration.titleView.insets.left - ), - self.titleViewController.view.trailingAnchor.constraint( - equalTo: self.anchor.trailingAnchor, - constant: -configuration.titleView.insets.right - ) - ).store(in: self.constraintDisposeBag) - // Initialize ItemsView Top Anchor with BottomAnchor of the TitleViewController - itemsViewTopAnchor = self.titleViewController.view.bottomAnchor - // Initialize ItemsView Top Anchor Constants with ItemsView top insets and TitleView Bottom Insets - itemsViewTopAnchorConstant = configuration.itemsView.insets.top + configuration.titleView.insets.bottom - case .scrolls: - // In Scrolls Mode the TitleViewController is added as the TableHeaderView of - // the ItemsViewController so there is no need to make constraints - // Initialize ItemsView Top Anchor with the Views TopAnchor - itemsViewTopAnchor = self.anchor.topAnchor - // Initialize ItemsView Top Anchor Constants with the ItemsView Top insets - itemsViewTopAnchorConstant = configuration.itemsView.insets.top - } - // Make Constraints on ItemsViewController - self.itemsViewController.view.makeConstraints( - self.itemsViewController.view.topAnchor.constraint( - equalTo: itemsViewTopAnchor, - constant: itemsViewTopAnchorConstant - ), - self.itemsViewController.view.leadingAnchor.constraint( - equalTo: self.anchor.leadingAnchor, - constant: configuration.itemsView.insets.left - ), - self.itemsViewController.view.trailingAnchor.constraint( - equalTo: self.anchor.trailingAnchor, - constant: -configuration.itemsView.insets.right - ), - self.itemsViewController.view.bottomAnchor.constraint( - equalTo: self.buttonViewController.view.topAnchor, - constant: -( - configuration.itemsView.insets.bottom - + configuration.completionButton.insets.top - ) - ) - ).store(in: self.constraintDisposeBag) - // Make Constraints on ButtonViewController - self.buttonViewController.view.makeConstraints( - self.buttonViewController.view.leadingAnchor.constraint( - equalTo: self.anchor.leadingAnchor, - constant: configuration.completionButton.insets.left - ), - self.buttonViewController.view.trailingAnchor.constraint( - equalTo: self.anchor.trailingAnchor, - constant: -configuration.completionButton.insets.right - ), - self.buttonViewController.view.bottomAnchor.constraint( - equalTo: self.anchor.bottomAnchor, - constant: -configuration.completionButton.insets.bottom - ) - ).store(in: self.constraintDisposeBag) - } - -} - -// MARK: - Handle Button Press - -extension WhatsNewViewController { - - /// Handle Button Press - /// - /// - Parameter buttonType: The Button type - func handlePress( - buttonType: WhatsNewButtonViewController.ButtonType - ) { - // Switch on button type - switch buttonType { - case .completion: - // Invoke HapticFeebdack for completion button - self.configuration.completionButton.hapticFeedback?.execute() - // Store WhatsNew Version - self.storeWhatsNewVersion() - // Switch on CompletionAction - switch self.configuration.completionButton.action { - case .dismiss: - // Dismiss WhatsNewViewController - self.dismiss(animated: true) - case .custom(action: let action): - // Perform custom action and pass self - action(self) - } - case .detail: - // Invoke HapticFeebdack for detail button - self.configuration.detailButton?.hapticFeedback?.execute() - // Switch on DetailAction - switch self.configuration.detailButton?.action { - case .some(.website(let urlString)): - // Check if url is available - guard let url = URL(string: urlString) else { - // URL unavailable - return - } - // Initialize SafariViewController - let safariViewController = SFSafariViewController(url: url) - // Check if iOS 10 or greater is available - if #available(iOS 10.0, *) { - // Set tint color - safariViewController.preferredControlTintColor = self.configuration.tintColor - // Set Bar tint Color - safariViewController.preferredBarTintColor = self.configuration.backgroundColor - } - // Present ViewController - self.present(safariViewController, animated: true) - case .some(.custom(action: let action)): - // Perform custom action and pass self - action(self) - case .none: - break - } - } - } - -} - -// MARK: - Store Version - -extension WhatsNewViewController { - - /// Store presented WhatsNew Version - func storeWhatsNewVersion() { - // Store Version if VersionStore is available - self.versionStore?.set(version: self.whatsNew.version) - // Clear VersionStore - self.versionStore = nil - } - -} - -// MARK: Present/Push - -public extension WhatsNewViewController { - - /// Present WhatsNewViewController - /// - /// - Parameters: - /// - viewController: The ViewController to present on - /// - animated: If present should be animated. Default value `true` - /// - completion: The completion closure. Default value `nil` - func present( - on viewController: UIViewController?, - animated: Bool = true, - completion: (() -> Void)? = nil - ) { - // Present WhatsNewViewController - viewController?.present(self, animated: animated, completion: completion) - } - - /// Push WhatsNewViewController - /// - /// - Parameters: - /// - navigationController: The NavigationController - /// - animated: Should be pushed animated. Default value `true` - func push( - on navigationController: UINavigationController?, - animated: Bool = true - ) { - // Push WhatsNewViewController - navigationController?.pushViewController(self, animated: animated) - } - -} diff --git a/Tests/BaseTests.swift b/Tests/BaseTests.swift deleted file mode 100644 index 878fd8c..0000000 --- a/Tests/BaseTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// BaseTests.swift -// TveeeKit -// -// Created by Sven Tiigi on 09.04.18. -// Copyright © 2018 opwoco GmbH. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class BaseTests: XCTestCase { - - /// The timeout value while waiting - /// that an expectation is fulfilled - lazy var expectationTimeout: TimeInterval = 10.0 - - /// Random String - lazy var randomString: String = self.generateRandomString() - - /// Random Data - lazy var randomData: Data = self.generateRandomData() - - /// Random Int - lazy var randomInt: Int = self.generateRandomInt() - - /// Random Double - lazy var randomDouble: Double = self.generateRandomDouble() - - /// Random WhatsNew - lazy var randomWhatsNew: WhatsNew = self.generateRandomWhatsNew() - - /// System Image - lazy var image: UIImage? = UIButton(type: .infoLight).image(for: .normal) - - /// SetUp - override func setUp() { - super.setUp() - // Disable continueAfterFailure - self.continueAfterFailure = false - } - - /// Perform test with expectation - /// - /// - Parameters: - /// - name: The expectation name - /// - timeout: The optional custom timeout - /// - execution: The test execution - /// - completionHandler: The optional XCWaitCompletionHandler - func performTest(name: String = "\(#function): Line \(#line)", - timeout: TimeInterval? = nil, - execution: (XCTestExpectation) -> Void, - completionHandler: XCWaitCompletionHandler? = nil) { - // Create expectation with function name - let expectation = self.expectation(description: name) - // Perform test execution with expectation - execution(expectation) - // Wait for expectation been fulfilled with custom or default timeout - self.waitForExpectations( - timeout: timeout.flatMap { $0 } ?? self.expectationTimeout, - handler: completionHandler - ) - } - - /// Generate a random String - /// - /// - Returns: A random string - func generateRandomString() -> String { - return UUID().uuidString - } - - /// Generate a random Int - /// - /// - Returns: A random Int - func generateRandomInt() -> Int { - return Int(arc4random_uniform(100)) - } - - /// Generate a random Double - /// - /// - Returns: A random Double - func generateRandomDouble() -> Double { - return Double(self.generateRandomInt()) - } - - /// Generate a random Data object - /// - /// - Returns: Random Data object - func generateRandomData() -> Data { - return Data(self.generateRandomString().utf8) - } - - /// Generate a random WhatsNew - func generateRandomWhatsNew() -> WhatsNew { - return WhatsNew( - version: .init( - major: self.generateRandomInt(), - minor: self.generateRandomInt(), - patch: self.generateRandomInt()), - title: self.generateRandomString(), - items: [ - WhatsNew.Item( - title: self.generateRandomString(), - subtitle: self.generateRandomString(), - image: nil - ) - ] - ) - } - -} diff --git a/Tests/Models/WhatsNewItemTests.swift b/Tests/Models/WhatsNewItemTests.swift deleted file mode 100644 index 416411e..0000000 --- a/Tests/Models/WhatsNewItemTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// WhatsNewItemTests.swift -// WhatsNewKit-iOS Tests -// -// Created by Sven Tiigi on 01.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class WhatsNewItemTests: BaseTests { - - func testCodableWithImage() { - let whatsNewItem = WhatsNew.Item( - title: self.generateRandomString(), - subtitle: self.generateRandomString(), - image: self.image - ) - let data: Data - do { - data = try JSONEncoder().encode(whatsNewItem) - } catch { - XCTFail(error.localizedDescription) - return - } - let whatsNewItemDecoded: WhatsNew.Item - do { - whatsNewItemDecoded = try JSONDecoder().decode(WhatsNew.Item.self, from: data) - } catch { - XCTFail(error.localizedDescription) - return - } - XCTAssertEqual(whatsNewItem.title, whatsNewItemDecoded.title) - XCTAssertEqual(whatsNewItem.subtitle, whatsNewItemDecoded.subtitle) - XCTAssertNotNil(whatsNewItemDecoded.image) - } - - func testCodableNoImage() { - let whatsNewItem = WhatsNew.Item( - title: self.generateRandomString(), - subtitle: self.generateRandomString(), - image: nil - ) - let data: Data - do { - data = try JSONEncoder().encode(whatsNewItem) - } catch { - XCTFail(error.localizedDescription) - return - } - let whatsNewItemDecoded: WhatsNew.Item - do { - whatsNewItemDecoded = try JSONDecoder().decode(WhatsNew.Item.self, from: data) - } catch { - XCTFail(error.localizedDescription) - return - } - XCTAssertEqual(whatsNewItem.title, whatsNewItemDecoded.title) - XCTAssertEqual(whatsNewItem.subtitle, whatsNewItemDecoded.subtitle) - XCTAssertNil(whatsNewItem.image) - } - -} diff --git a/Tests/Models/WhatsNewTests.swift b/Tests/Models/WhatsNewTests.swift deleted file mode 100644 index 6de27bc..0000000 --- a/Tests/Models/WhatsNewTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// WhatsNewTests.swift -// WhatsNewKit-iOS -// -// Created by Sven Tiigi on 24.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class WhatsNewTests: BaseTests { - - func testGetByVersion() { - let whatsNew1 = self.generateRandomWhatsNew() - let whatsNew2 = self.generateRandomWhatsNew() - let targetVersion = whatsNew1.version - let whatsNews = [whatsNew1, whatsNew2] - XCTAssertEqual(whatsNew1, whatsNews.get(byVersion: targetVersion)) - } - - func testGetByBundle() { - let whatsNew1 = self.generateRandomWhatsNew() - let whatsNew2 = self.generateRandomWhatsNew() - class FakeBundle: Bundle { - var version: WhatsNew.Version? - override var infoDictionary: [String : Any]? { - guard let version = self.version?.description else { - return nil - } - return ["CFBundleShortVersionString": version] - } - } - let fakeBundle = FakeBundle() - fakeBundle.version = whatsNew1.version - let whatsNews = [whatsNew1, whatsNew2] - XCTAssertEqual(whatsNew1, whatsNews.get(byBundle: fakeBundle)) - } - -} diff --git a/Tests/Models/WhatsNewVersionTests.swift b/Tests/Models/WhatsNewVersionTests.swift deleted file mode 100644 index 722c832..0000000 --- a/Tests/Models/WhatsNewVersionTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// WhatsNewVersionTests.swift -// WhatsNewKit-iOS Tests -// -// Created by Sven Tiigi on 24.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class WhatsNewVersionTests: BaseTests { - - func testStringLiteral() { - let major = self.generateRandomInt() - let minor = self.generateRandomInt() - let patch = self.generateRandomInt() - let versionString = "\(major).\(minor).\(patch)" - let version = WhatsNew.Version(stringLiteral: versionString) - XCTAssertEqual(major, version.major) - XCTAssertEqual(minor, version.minor) - XCTAssertEqual(patch, version.patch) - XCTAssertEqual(versionString, version.description) - } - - func testInvalidStringLiteral() { - let version = WhatsNew.Version(stringLiteral: "") - XCTAssertEqual(0, version.major) - XCTAssertEqual(0, version.minor) - XCTAssertEqual(0, version.patch) - } - - func testCurrentInBundle() { - class FakeBundle: Bundle { - override var infoDictionary: [String : Any]? { - return ["CFBundleShortVersionString": self.version] - } - var version: String = "" - } - let major = self.generateRandomInt() - let minor = self.generateRandomInt() - let patch = self.generateRandomInt() - let versionString = "\(major).\(minor).\(patch)" - let fakeBundle = FakeBundle() - fakeBundle.version = versionString - let version = WhatsNew.Version.current(inBundle: fakeBundle) - XCTAssertEqual(major, version.major) - XCTAssertEqual(minor, version.minor) - XCTAssertEqual(patch, version.patch) - XCTAssertEqual(versionString, version.description) - } - - func testCurrentInBundleUnavailable() { - class FakeBundle: Bundle { - override var infoDictionary: [String : Any]? { - return nil - } - } - let fakeBundle = FakeBundle() - let version = WhatsNew.Version.current(inBundle: fakeBundle) - XCTAssertEqual(0, version.major) - XCTAssertEqual(0, version.minor) - XCTAssertEqual(0, version.patch) - XCTAssertEqual("0.0.0", version.description) - } - - func testComparable() { - let version0 = WhatsNew.Version(major: 0, minor: 0, patch: 1) - let version1 = WhatsNew.Version(major: 1, minor: 0, patch: 0) - XCTAssert(version0 < version1) - XCTAssert(version1 > version0) - } - - func testLargeMinorVersionComparable() { - let version0 = WhatsNew.Version(major: 3, minor: 17, patch: 7) - let version1 = WhatsNew.Version(major: 4, minor: 7, patch: 7) - XCTAssertTrue(version0 < version1) - XCTAssertTrue(version1 > version0) - XCTAssertFalse(version1 == version0) - } - - func testEquality() { - let version0 = WhatsNew.Version(major: 1, minor: 0, patch: 0) - let version1 = WhatsNew.Version(major: 1, minor: 0, patch: 0) - XCTAssertFalse(version0 < version1) - XCTAssertFalse(version1 > version0) - XCTAssertTrue(version1 == version0) - } - -} diff --git a/Tests/Stores/InMemoryWhatsNewVersionStoreTests.swift b/Tests/Stores/InMemoryWhatsNewVersionStoreTests.swift deleted file mode 100644 index 8c9f4a7..0000000 --- a/Tests/Stores/InMemoryWhatsNewVersionStoreTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// InMemoryWhatsNewVersionStoreTests.swift -// WhatsNewKit-iOS Tests -// -// Created by Sven Tiigi on 01.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class InMemoryWhatsNewVersionStoreTests: BaseTests { - - func testInMemory() { - let randomWhatsNewVersion = self.randomWhatsNew.version - let inMemoryWhatsNewVersionStore = InMemoryWhatsNewVersionStore() - XCTAssertFalse(inMemoryWhatsNewVersionStore.has(version: randomWhatsNewVersion)) - inMemoryWhatsNewVersionStore.set(version: randomWhatsNewVersion) - XCTAssert(inMemoryWhatsNewVersionStore.has(version: randomWhatsNewVersion)) - inMemoryWhatsNewVersionStore.clearVersions() - XCTAssertFalse(inMemoryWhatsNewVersionStore.has(version: randomWhatsNewVersion)) - } - -} diff --git a/Tests/Stores/KeyValueWhatsNewVersionStoreTests.swift b/Tests/Stores/KeyValueWhatsNewVersionStoreTests.swift deleted file mode 100644 index 4f57914..0000000 --- a/Tests/Stores/KeyValueWhatsNewVersionStoreTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// KeyValueWhatsNewVersionStoreTests.swift -// WhatsNewKit-iOS Tests -// -// Created by Sven Tiigi on 01.06.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - -import XCTest -@testable import WhatsNewKit - -class KeyValueWhatsNewVersionStoreTests: BaseTests { - - func testKeyValueStore() { - class FakeKeyValueable: KeyValueable { - var objects: [String: Any] = [:] - func set(_ value: Any?, forKey key: String) { - self.objects[key] = value - } - func object(forKey: String) -> Any? { - return self.objects[forKey] - } - } - let fakeKeyValueable = FakeKeyValueable() - let keyValueWhatsNewVersionStore = KeyValueWhatsNewVersionStore(keyValueable: fakeKeyValueable, prefixIdentifier: "unit.test") - let randomWhatsNewVersion = self.randomWhatsNew.version - keyValueWhatsNewVersionStore.set(version: randomWhatsNewVersion) - XCTAssertEqual(randomWhatsNewVersion.description, fakeKeyValueable.objects["unit.test.\(randomWhatsNewVersion.description)"] as? String) - XCTAssert(keyValueWhatsNewVersionStore.has(version: randomWhatsNewVersion)) - XCTAssertFalse(keyValueWhatsNewVersionStore.has(version: self.generateRandomWhatsNew().version)) - } - - func testKeyValueWhatsNewVersionNSUbiquitousKeyValueStore() { - let iCloudKeyValue = NSUbiquitousKeyValueStore.default - let version = self.randomWhatsNew.version - let versionKey = "unit.test.\(version.description)" - iCloudKeyValue.removeObject(forKey: version.description) - XCTAssertNil(iCloudKeyValue.object(forKey: versionKey)) - let keyValueWhatsNewVersionStore = KeyValueWhatsNewVersionStore(keyValueable: iCloudKeyValue, prefixIdentifier: "unit.test") - XCTAssertFalse(keyValueWhatsNewVersionStore.has(version: version)) - keyValueWhatsNewVersionStore.set(version: version) - XCTAssertTrue(keyValueWhatsNewVersionStore.has(version: version)) - XCTAssertEqual(iCloudKeyValue.object(forKey: versionKey) as? String, version.description) - iCloudKeyValue.removeObject(forKey: version.description) - } - - func testKeyValueWhatsNewVersionUserDefaultsStore() { - let userDefaults = UserDefaults.standard - let version = self.randomWhatsNew.version - let versionKey = "unit.test.\(version.description)" - userDefaults.removeObject(forKey: versionKey) - XCTAssertNil(userDefaults.object(forKey: versionKey)) - let keyValueWhatsNewVersionStore = KeyValueWhatsNewVersionStore(keyValueable: userDefaults, prefixIdentifier: "unit.test") - XCTAssertFalse(keyValueWhatsNewVersionStore.has(version: version)) - keyValueWhatsNewVersionStore.set(version: version) - XCTAssertTrue(keyValueWhatsNewVersionStore.has(version: version)) - XCTAssertEqual(userDefaults.object(forKey: versionKey) as? String, version.description) - userDefaults.removeObject(forKey: versionKey) - } - -} - diff --git a/Tests/WhatsNewEnvironmentTests.swift b/Tests/WhatsNewEnvironmentTests.swift new file mode 100644 index 0000000..4cd09e8 --- /dev/null +++ b/Tests/WhatsNewEnvironmentTests.swift @@ -0,0 +1,103 @@ +import XCTest +@testable import WhatsNewKit + +// MARK: - WhatsNewEnvironmentTests + +/// The WhatsNewEnvironmentTests +final class WhatsNewEnvironmentTests: WhatsNewKitTestCase { + + func testInitial() { + let version_1_0_0: WhatsNew.Version = "1.0.0" + let whatsNew_1_0_0 = self.makeWhatsNew(from: version_1_0_0) + let versionStore = InMemoryWhatsNewVersionStore() + let environment = WhatsNewEnvironment( + currentVersion: version_1_0_0, + versionStore: versionStore, + whatsNewCollection: [whatsNew_1_0_0] + ) + XCTAssertEqual( + version_1_0_0, + environment.whatsNew()?.version + ) + } + + func testInitialReopen() { + let version_1_0_0: WhatsNew.Version = "1.0.0" + let whatsNew_1_0_0 = self.makeWhatsNew(from: version_1_0_0) + let versionStore = InMemoryWhatsNewVersionStore() + versionStore.save(presentedVersion: version_1_0_0) + let environment = WhatsNewEnvironment( + currentVersion: version_1_0_0, + versionStore: versionStore, + whatsNewCollection: [whatsNew_1_0_0] + ) + XCTAssertNil( + environment.whatsNew() + ) + } + + func testUpdate() { + let version_1_0_0: WhatsNew.Version = "1.0.0" + let whatsNew_1_0_0 = self.makeWhatsNew(from: version_1_0_0) + let version_1_0_1: WhatsNew.Version = "1.0.1" + let whatsNew_1_0_1 = self.makeWhatsNew(from: version_1_0_1) + let versionStore = InMemoryWhatsNewVersionStore() + versionStore.save(presentedVersion: version_1_0_0) + let environment = WhatsNewEnvironment( + currentVersion: version_1_0_1, + versionStore: versionStore, + whatsNewCollection: [ + whatsNew_1_0_0, + whatsNew_1_0_1 + ] + .shuffled() + ) + XCTAssertEqual( + version_1_0_1, + environment.whatsNew()?.version + ) + } + + func testInitialAfterUpdates() { + let version_1_0_0: WhatsNew.Version = "1.0.0" + let whatsNew_1_0_0 = self.makeWhatsNew(from: version_1_0_0) + let version_1_1_0: WhatsNew.Version = "1.1.0" + let whatsNew_1_1_0 = self.makeWhatsNew(from: version_1_1_0) + let versionStore = InMemoryWhatsNewVersionStore() + let environment = WhatsNewEnvironment( + currentVersion: "1.1.1", + versionStore: versionStore, + whatsNewCollection: [ + whatsNew_1_0_0, + whatsNew_1_1_0 + ] + .shuffled() + ) + XCTAssertEqual( + version_1_1_0, + environment.whatsNew()?.version + ) + } + + func testInitialAfterUpdatesReopen() { + let version_1_0_0: WhatsNew.Version = "1.0.0" + let whatsNew_1_0_0 = self.makeWhatsNew(from: version_1_0_0) + let version_1_1_0: WhatsNew.Version = "1.1.0" + let whatsNew_1_1_0 = self.makeWhatsNew(from: version_1_1_0) + let versionStore = InMemoryWhatsNewVersionStore() + versionStore.save(presentedVersion: version_1_1_0) + let environment = WhatsNewEnvironment( + currentVersion: "1.1.1", + versionStore: versionStore, + whatsNewCollection: [ + whatsNew_1_0_0, + whatsNew_1_1_0 + ] + .shuffled() + ) + XCTAssertNil( + environment.whatsNew() + ) + } + +} diff --git a/Tests/WhatsNewKitTestCase.swift b/Tests/WhatsNewKitTestCase.swift new file mode 100644 index 0000000..544154e --- /dev/null +++ b/Tests/WhatsNewKitTestCase.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import WhatsNewKit + +// MARK: - WhatsNewKitTestCase + +/// The WhatsNewKitTestCase +class WhatsNewKitTestCase: XCTestCase { + + // MARK: XCTestCase-Lifecycle + + /// Provides an opportunity to reset state before calling each test method in a test case. + override func setUp() { + super.setUp() + // Disable continueAfterFailure + self.continueAfterFailure = false + } + + // MARK: Convenience Functions + + /// Make a random WhatsNew Version + func makeRandomWhatsNewVersion() -> WhatsNew.Version { + let randomVersionRange = 0...9 + return .init( + major: .random(in: randomVersionRange), + minor: .random(in: randomVersionRange), + patch: .random(in: randomVersionRange) + ) + } + + /// Create a WhatsNew instance from a given WhatsNew Version + /// - Parameter version: The WhatsNew Version + func makeWhatsNew( + from version: WhatsNew.Version + ) -> WhatsNew { + .init( + version: version, + title: "", + features: .init() + ) + } + +} diff --git a/Tests/WhatsNewVersionStoreTests.swift b/Tests/WhatsNewVersionStoreTests.swift new file mode 100644 index 0000000..42e0d96 --- /dev/null +++ b/Tests/WhatsNewVersionStoreTests.swift @@ -0,0 +1,111 @@ +import XCTest +@testable import WhatsNewKit + +// MARK: - WhatsNewVersionStoreTests + +/// The WhatsNewVersionStoreTests +final class WhatsNewVersionStoreTests: WhatsNewKitTestCase { + + func testInMemoryWhatsNewVersionStore() { + let inMemoryWhatsNewVersionStore = InMemoryWhatsNewVersionStore() + let version = self.executeVersionStoreTest(inMemoryWhatsNewVersionStore) + XCTAssertEqual( + [version], + inMemoryWhatsNewVersionStore.versions + ) + inMemoryWhatsNewVersionStore.removeAll() + XCTAssert( + inMemoryWhatsNewVersionStore.presentedVersions.isEmpty + ) + XCTAssert( + inMemoryWhatsNewVersionStore.versions.isEmpty + ) + } + + func testUserDefaultsWhatsNewVersionStore() { + final class FakeUserDefaults: UserDefaults { + var store: [String: Any] = .init() + + override func set(_ value: Any?, forKey defaultName: String) { + self.store[defaultName] = value + } + + override func dictionaryRepresentation() -> [String: Any] { + self.store + } + } + let fakeUserDefaults = FakeUserDefaults() + let userDefaultsWhatsNewVersionStore = UserDefaultsWhatsNewVersionStore( + userDefaults: fakeUserDefaults + ) + let version = self.executeVersionStoreTest(userDefaultsWhatsNewVersionStore) + XCTAssertEqual( + fakeUserDefaults.store.count, + 1 + ) + XCTAssertEqual( + version, + (fakeUserDefaults.store[version.key] as? String).flatMap(WhatsNew.Version.init) + ) + userDefaultsWhatsNewVersionStore.removeAll() + XCTAssert( + userDefaultsWhatsNewVersionStore.presentedVersions.isEmpty + ) + XCTAssert( + fakeUserDefaults.store.isEmpty + ) + } + + func testNSUbiquitousKeyValueWhatsNewVersionStore() { + final class FakeNSUbiquitousKeyValueStore: NSUbiquitousKeyValueStore { + var store: [String: Any] = .init() + + override var dictionaryRepresentation: [String: Any] { + self.store + } + + override func set(_ value: Any?, forKey defaultName: String) { + self.store[defaultName] = value + } + } + let fakeNSUbiquitousKeyValueStore = FakeNSUbiquitousKeyValueStore() + let ubiquitousKeyValueWhatsNewVersionStore = NSUbiquitousKeyValueWhatsNewVersionStore( + ubiquitousKeyValueStore: fakeNSUbiquitousKeyValueStore + ) + let version = self.executeVersionStoreTest(ubiquitousKeyValueWhatsNewVersionStore) + XCTAssertEqual( + fakeNSUbiquitousKeyValueStore.store.count, + 1 + ) + XCTAssertEqual( + version, + (fakeNSUbiquitousKeyValueStore.store[version.key] as? String).flatMap(WhatsNew.Version.init) + ) + ubiquitousKeyValueWhatsNewVersionStore.removeAll() + XCTAssert( + ubiquitousKeyValueWhatsNewVersionStore.presentedVersions.isEmpty + ) + XCTAssert( + fakeNSUbiquitousKeyValueStore.store.isEmpty + ) + } + +} + +private extension WhatsNewVersionStoreTests { + + func executeVersionStoreTest( + _ versionStore: WhatsNewVersionStore + ) -> WhatsNew.Version { + let version = self.makeRandomWhatsNewVersion() + XCTAssert(versionStore.presentedVersions.isEmpty) + XCTAssertFalse(versionStore.hasPresented(version)) + versionStore.save( + presentedVersion: version + ) + XCTAssertEqual([version], versionStore.presentedVersions) + XCTAssert(versionStore.hasPresented(version)) + return version + } + +} diff --git a/Tests/WhatsNewVersionTests.swift b/Tests/WhatsNewVersionTests.swift new file mode 100644 index 0000000..7ef42aa --- /dev/null +++ b/Tests/WhatsNewVersionTests.swift @@ -0,0 +1,85 @@ +import XCTest +@testable import WhatsNewKit + +// MARK: - WhatsNewVersionTests + +/// The WhatsNewVersionTests +final class WhatsNewVersionTests: WhatsNewKitTestCase { + + func testStringLiteral() { + let whatsNewVersionString = "9.9.9" + let whatsNewVersion = WhatsNew.Version(stringLiteral: whatsNewVersionString) + XCTAssertEqual( + whatsNewVersionString, + whatsNewVersion.description + ) + } + + func testBadStringLiteral() { + let whatsNewVersionString = UUID().uuidString + let whatsNewVersion = WhatsNew.Version(stringLiteral: whatsNewVersionString) + XCTAssertEqual( + "0.0.0", + whatsNewVersion.description + ) + } + + func testComparable() { + let sortedVersions: [WhatsNew.Version] = [ + "1.0.0", + "1.0.1", + "1.1.1", + "1.1.2", + "1.2.0", + "2.0.0", + "2.0.1", + "2.1.0" + ] + XCTAssertEqual( + sortedVersions, + sortedVersions.shuffled().sorted(by: <) + ) + } + + func testCurrent() { + class FakeBundle: Bundle { + let shortVersionString: String + init(shortVersionString: String) { + self.shortVersionString = shortVersionString + super.init() + } + override var infoDictionary: [String : Any]? { + [ + "CFBundleShortVersionString": self.shortVersionString + ] + } + } + let version = self.makeRandomWhatsNewVersion() + let fakeBundle = FakeBundle(shortVersionString: version.description) + XCTAssertEqual( + version, + WhatsNew.Version.current(in: fakeBundle) + ) + let fakeBundleEmptyVersion = FakeBundle(shortVersionString: "") + XCTAssertEqual( + WhatsNew.Version(major: 0, minor: 0, patch: 0), + WhatsNew.Version.current(in: fakeBundleEmptyVersion) + ) + } + + func testKeyPrefix() { + XCTAssertEqual( + "WhatsNewKit", + WhatsNew.Version.keyPrefix + ) + } + + func testKey() { + let version = self.makeRandomWhatsNewVersion() + XCTAssertEqual( + "WhatsNewKit.\(version.description)", + version.key + ) + } + +} diff --git a/Tests/WhatsNewViewControllerTests.swift b/Tests/WhatsNewViewControllerTests.swift index e5d3465..99933c1 100644 --- a/Tests/WhatsNewViewControllerTests.swift +++ b/Tests/WhatsNewViewControllerTests.swift @@ -1,124 +1,27 @@ -// -// WhatsNewViewControllerTests.swift -// WhatsNewKit-iOS Tests -// -// Created by Sven Tiigi on 26.05.18. -// Copyright © 2018 WhatsNewKit. All rights reserved. -// - import XCTest @testable import WhatsNewKit -class WhatsNewViewControllerTests: BaseTests { - - func testAvailableVersionInVersionStore() { - let versionStore = InMemoryWhatsNewVersionStore() - versionStore.set(version: self.randomWhatsNew.version) - XCTAssertNil(WhatsNewViewController(whatsNew: self.randomWhatsNew, versionStore: versionStore)) - } +// MARK: - WhatsNewViewControllerTests + +/// The WhatsNewViewControllerTests +final class WhatsNewViewControllerTests: WhatsNewKitTestCase { - func testUnavailableVersionInVersionStore() { + func testInitializer() { let versionStore = InMemoryWhatsNewVersionStore() - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew, versionStore: versionStore) - XCTAssertNotNil(whatsNewViewController) - whatsNewViewController?.handlePress(buttonType: .completion) - XCTAssert(versionStore.has(version: self.randomWhatsNew.version)) - } - - func testWhatsNewPassedToControllerEquatable() { - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew) - XCTAssertEqual(self.randomWhatsNew, whatsNewViewController.whatsNew) - } - - func testCoderInitializer() { - XCTAssertNil(WhatsNewViewController(coder: .init())) - } - - func testBackgroundColor() { - let color = UIColor.purple - var configuration = WhatsNewViewController.Configuration() - configuration.backgroundColor = color - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew, configuration: configuration) - XCTAssertEqual(color, whatsNewViewController.view.backgroundColor) - } - - func testPreferredStatusBarStyleLightContent() { - let color = UIColor.black - var configuration = WhatsNewViewController.Configuration() - configuration.backgroundColor = color - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew, configuration: configuration) - XCTAssertEqual(.lightContent, whatsNewViewController.preferredStatusBarStyle) - } - - func testPreferredStatusBarStyleDefault() { - let color = UIColor.white - var configuration = WhatsNewViewController.Configuration() - configuration.backgroundColor = color - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew, configuration: configuration) - XCTAssertEqual(.default, whatsNewViewController.preferredStatusBarStyle) - } - - func testPadAdjustment() { - WhatsNewViewController.userInterfaceIdiom = .pad - self.performTest(execution: { expectation in - var configuration = WhatsNewViewController.Configuration() - configuration.padAdjustment = { _ in - expectation.fulfill() - } - let controller = WhatsNewViewController(whatsNew: self.randomWhatsNew, configuration: configuration) - controller.traitCollectionDidChange(nil) - }, completionHandler: { _ in - WhatsNewViewController.userInterfaceIdiom = UIDevice.current.userInterfaceIdiom - }) - } - - func testNoPadAdjustment() { - WhatsNewViewController.userInterfaceIdiom = .phone - var configuration = WhatsNewViewController.Configuration() - configuration.padAdjustment = { _ in - XCTFail("Pad Adjustment mustn't be called when UserInterfaceIdiom is Phone") - } - _ = WhatsNewViewController(whatsNew: self.randomWhatsNew, configuration: configuration) - WhatsNewViewController.userInterfaceIdiom = UIDevice.current.userInterfaceIdiom - } - - func testPresent() { - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew) - class FakeViewController: UIViewController { - var viewControllerToPresent: UIViewController? - var shouldAnimate: Bool? - override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { - self.viewControllerToPresent = viewControllerToPresent - self.shouldAnimate = flag - completion?() - } - } - let fakeViewViewController = FakeViewController() - let animated = true - self.performTest(execution: { expectation in - whatsNewViewController.present(on: fakeViewViewController, animated: animated, completion: { - XCTAssert(fakeViewViewController.viewControllerToPresent === whatsNewViewController) - XCTAssertEqual(fakeViewViewController.shouldAnimate, true) - expectation.fulfill() - }) - }) - } - - func testPush() { - let whatsNewViewController = WhatsNewViewController(whatsNew: self.randomWhatsNew) - class FakeNavigationController: UINavigationController { - var pushedViewController: UIViewController? - var shouldAnimate: Bool? - override func pushViewController(_ viewController: UIViewController, animated: Bool) { - self.pushedViewController = viewController - self.shouldAnimate = animated - } - } - let fakeNavigationController = FakeNavigationController() - let animated = true - whatsNewViewController.push(on: fakeNavigationController, animated: animated) - XCTAssert(fakeNavigationController.pushedViewController === whatsNewViewController) - XCTAssertEqual(fakeNavigationController.shouldAnimate, animated) + let whatsNew = self.makeWhatsNew(from: self.makeRandomWhatsNewVersion()) + XCTAssertNotNil( + WhatsNewViewController( + whatsNew: whatsNew, + versionStore: versionStore + ) + ) + versionStore.save(presentedVersion: whatsNew.version) + XCTAssertNil( + WhatsNewViewController( + whatsNew: whatsNew, + versionStore: versionStore + ) + ) } } diff --git a/WhatsNewKit.podspec b/WhatsNewKit.podspec deleted file mode 100644 index 25afdcc..0000000 --- a/WhatsNewKit.podspec +++ /dev/null @@ -1,14 +0,0 @@ -Pod::Spec.new do |s| - s.name = "WhatsNewKit" - s.version = "1.3.7" - s.summary = "Showcase your awesome new app features" - s.homepage = "https://github.com/SvenTiigi/WhatsNewKit" - s.social_media_url = 'http://twitter.com/SvenTiigi' - s.license = 'MIT' - s.author = { "Sven Tiigi" => "sven.tiigi@gmail.com" } - s.source = { :git => "https://github.com/SvenTiigi/WhatsNewKit.git", :tag => s.version.to_s } - s.swift_version = "5.0" - s.ios.deployment_target = "9.0" - s.source_files = 'Sources/**/*' - s.frameworks = 'Foundation', 'UIKit' -end diff --git a/WhatsNewKit.xcodeproj/project.pbxproj b/WhatsNewKit.xcodeproj/project.pbxproj deleted file mode 100644 index 9437a38..0000000 --- a/WhatsNewKit.xcodeproj/project.pbxproj +++ /dev/null @@ -1,1027 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 47; - objects = { - -/* Begin PBXBuildFile section */ - 2264FE192441B2630030A4CE /* TitleModeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2264FE182441B2630030A4CE /* TitleModeConfiguration.swift */; }; - 3D04214620C2D68C00DA5126 /* BaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214520C2D68C00DA5126 /* BaseTests.swift */; }; - 3D04215120C2D6A100DA5126 /* WhatsNewViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214720C2D6A000DA5126 /* WhatsNewViewControllerTests.swift */; }; - 3D04215320C2D6A100DA5126 /* WhatsNewVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214B20C2D6A100DA5126 /* WhatsNewVersionTests.swift */; }; - 3D04215420C2D6A100DA5126 /* WhatsNewItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214C20C2D6A100DA5126 /* WhatsNewItemTests.swift */; }; - 3D04215520C2D6A100DA5126 /* WhatsNewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214D20C2D6A100DA5126 /* WhatsNewTests.swift */; }; - 3D04215620C2D6A100DA5126 /* KeyValueWhatsNewVersionStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04214F20C2D6A100DA5126 /* KeyValueWhatsNewVersionStoreTests.swift */; }; - 3D04215720C2D6A100DA5126 /* InMemoryWhatsNewVersionStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D04215020C2D6A100DA5126 /* InMemoryWhatsNewVersionStoreTests.swift */; }; - 3D13455E20BC8AFA00BE0FF8 /* WhatsNewViewController+HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13455D20BC8AFA00BE0FF8 /* WhatsNewViewController+HapticFeedback.swift */; }; - 3D2412C720BADBDC005A04D6 /* KeyValueWhatsNewVersionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2412C620BADBDC005A04D6 /* KeyValueWhatsNewVersionStore.swift */; }; - 3D2412CF20BAE75E005A04D6 /* WhatsNewViewController+DetailButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2412CE20BAE75E005A04D6 /* WhatsNewViewController+DetailButton.swift */; }; - 3D2412D120BAE7C6005A04D6 /* WhatsNewViewController+CompletionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2412D020BAE7C6005A04D6 /* WhatsNewViewController+CompletionButton.swift */; }; - 3D2803CD22059B2200E1FA31 /* Anchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803CB220598F400E1FA31 /* Anchor.swift */; }; - 3D2803D02205A1F500E1FA31 /* WhatsNewTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803CE2205A1B800E1FA31 /* WhatsNewTitleViewController.swift */; }; - 3D2803D52205A6B600E1FA31 /* WhatsNewButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803D42205A6B600E1FA31 /* WhatsNewButtonViewController.swift */; }; - 3D2803D72205AA7F00E1FA31 /* WhatsNewButtonViewController+DetailButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803D62205AA7F00E1FA31 /* WhatsNewButtonViewController+DetailButton.swift */; }; - 3D2803D92205ABAB00E1FA31 /* WhatsNewButtonViewController+CompletionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803D82205ABAB00E1FA31 /* WhatsNewButtonViewController+CompletionButton.swift */; }; - 3D2803DB2205AE5C00E1FA31 /* WhatsNewItemsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803DA2205AE5C00E1FA31 /* WhatsNewItemsViewController.swift */; }; - 3D2803DD2205B4E200E1FA31 /* WhatsNewItemsViewController+Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803DC2205B4E200E1FA31 /* WhatsNewItemsViewController+Cell.swift */; }; - 3D2803E62205D4E800E1FA31 /* VersionStoreConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2803E52205D4E800E1FA31 /* VersionStoreConfiguration.swift */; }; - 3D299144217B5D8100132C91 /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299143217B5D8100132C91 /* ExampleViewController.swift */; }; - 3D299146217B5EDD00132C91 /* ConfigurationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299145217B5EDD00132C91 /* ConfigurationsTableViewCell.swift */; }; - 3D299148217B5FC700132C91 /* ConfigurationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299147217B5FC700132C91 /* ConfigurationCollectionViewCell.swift */; }; - 3D29914A217B691100132C91 /* ExampleViewController+PresentWhatsNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299149217B691100132C91 /* ExampleViewController+PresentWhatsNew.swift */; }; - 3D29914E217B6E7100132C91 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D29914D217B6E7100132C91 /* Configuration.swift */; }; - 3D299150217B6E8500132C91 /* BackgroundColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D29914F217B6E8500132C91 /* BackgroundColorConfiguration.swift */; }; - 3D299152217B6E9600132C91 /* TintColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299151217B6E9600132C91 /* TintColorConfiguration.swift */; }; - 3D299154217B6EA600132C91 /* AnimationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299153217B6EA600132C91 /* AnimationConfiguration.swift */; }; - 3D299156217B6EBA00132C91 /* LayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299155217B6EBA00132C91 /* LayoutConfiguration.swift */; }; - 3D29915D217B6FCD00132C91 /* Collection+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D29915C217B6FCD00132C91 /* Collection+Safe.swift */; }; - 3D29915F217B720400132C91 /* ReuseIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D29915E217B720400132C91 /* ReuseIdentifiable.swift */; }; - 3D299161217B7EEB00132C91 /* UIImage+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D299160217B7EEB00132C91 /* UIImage+Assets.swift */; }; - 3D330EC4217B8E8E002359E7 /* HapticFeedbackConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D330EC3217B8E8E002359E7 /* HapticFeedbackConfiguration.swift */; }; - 3D33221D21C416E900B2949F /* SecondaryTitleColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D33221C21C416E900B2949F /* SecondaryTitleColorConfiguration.swift */; }; - 3D375BA020BC6582005DE31B /* UIColor+Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D375B9F20BC6582005DE31B /* UIColor+Main.swift */; }; - 3D562CB920B3FD3200603182 /* WhatsNew+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D562CB820B3FD3200603182 /* WhatsNew+Version.swift */; }; - 3D5AA3BD252078C8002F6467 /* Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5AA3BC252078C8002F6467 /* Disposable.swift */; }; - 3D5AA3C0252078E6002F6467 /* DisposeBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5AA3BF252078E6002F6467 /* DisposeBag.swift */; }; - 3D5D06BD20C7C9C700EC7C46 /* WhatsNewViewController+TitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5D06BC20C7C9C700EC7C46 /* WhatsNewViewController+TitleView.swift */; }; - 3D5D06BF20C7CA2700EC7C46 /* WhatsNewViewController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5D06BE20C7CA2700EC7C46 /* WhatsNewViewController+Animation.swift */; }; - 3D5D06C120C7CAEC00EC7C46 /* WhatsNewViewController+ItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5D06C020C7CAEC00EC7C46 /* WhatsNewViewController+ItemsView.swift */; }; - 3D7E00C420B6A5F10036BDB1 /* WhatsNewViewController+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7E00C320B6A5F10036BDB1 /* WhatsNewViewController+Configuration.swift */; }; - 3D92C66A20AF87E600A10333 /* WhatsNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D92C66920AF87E600A10333 /* WhatsNew.swift */; }; - 3D92C66E20AF87F500A10333 /* WhatsNew+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D92C66D20AF87F500A10333 /* WhatsNew+Item.swift */; }; - 3D92C67120AF883300A10333 /* WhatsNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D92C67020AF883300A10333 /* WhatsNewViewController.swift */; }; - 3DA6679C20B0BB3B00DB18FF /* WhatsNewKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */; }; - 3DA6679D20B0BB3B00DB18FF /* WhatsNewKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 3DB507B924420E6C0014A479 /* UIView+MakeConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB507B824420E6C0014A479 /* UIView+MakeConstraints.swift */; }; - 3DBE9416236D7A6E0026B0FB /* WhatsNewViewController+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBE9414236D7A480026B0FB /* WhatsNewViewController+Theme.swift */; }; - 3DBFB67620B6EBC100FF0899 /* InMemoryWhatsNewVersionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBFB67520B6EBC100FF0899 /* InMemoryWhatsNewVersionStore.swift */; }; - 3DC8A6AB20AED02400292C94 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC8A6AA20AED02400292C94 /* AppDelegate.swift */; }; - 3DC8A6B220AED02600292C94 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3DC8A6B120AED02600292C94 /* Assets.xcassets */; }; - 3DC8A6B520AED02600292C94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3DC8A6B320AED02600292C94 /* LaunchScreen.storyboard */; }; - 3DDDB59720B832AB001D59C5 /* UIColor+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDDB59620B832AB001D59C5 /* UIColor+Defaults.swift */; }; - 3DF12C2B2210AE41006473F0 /* ContentModeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF12C2A2210AE41006473F0 /* ContentModeConfiguration.swift */; }; - 3DF12C2D2210AF51006473F0 /* ItemsCountConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF12C2C2210AF51006473F0 /* ItemsCountConfiguration.swift */; }; - 3DFB700020B34F8800673FD6 /* WhatsNewVersionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFB6FFF20B34F8800673FD6 /* WhatsNewVersionStore.swift */; }; - 52D6D9871BEFF229002C0205 /* WhatsNewKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 3DA6679E20B0BB3B00DB18FF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 52D6D97B1BEFF229002C0205; - remoteInfo = "WhatsNewKit-iOS"; - }; - 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 52D6D97B1BEFF229002C0205; - remoteInfo = WhatsNewKit; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 3DA667A020B0BB3B00DB18FF /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3DA6679D20B0BB3B00DB18FF /* WhatsNewKit.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 2264FE182441B2630030A4CE /* TitleModeConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleModeConfiguration.swift; sourceTree = ""; }; - 3D04214520C2D68C00DA5126 /* BaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTests.swift; sourceTree = ""; }; - 3D04214720C2D6A000DA5126 /* WhatsNewViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewViewControllerTests.swift; sourceTree = ""; }; - 3D04214B20C2D6A100DA5126 /* WhatsNewVersionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewVersionTests.swift; sourceTree = ""; }; - 3D04214C20C2D6A100DA5126 /* WhatsNewItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewItemTests.swift; sourceTree = ""; }; - 3D04214D20C2D6A100DA5126 /* WhatsNewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewTests.swift; sourceTree = ""; }; - 3D04214F20C2D6A100DA5126 /* KeyValueWhatsNewVersionStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyValueWhatsNewVersionStoreTests.swift; sourceTree = ""; }; - 3D04215020C2D6A100DA5126 /* InMemoryWhatsNewVersionStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryWhatsNewVersionStoreTests.swift; sourceTree = ""; }; - 3D13455D20BC8AFA00BE0FF8 /* WhatsNewViewController+HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+HapticFeedback.swift"; sourceTree = ""; }; - 3D2412C620BADBDC005A04D6 /* KeyValueWhatsNewVersionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueWhatsNewVersionStore.swift; sourceTree = ""; }; - 3D2412CE20BAE75E005A04D6 /* WhatsNewViewController+DetailButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+DetailButton.swift"; sourceTree = ""; }; - 3D2412D020BAE7C6005A04D6 /* WhatsNewViewController+CompletionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+CompletionButton.swift"; sourceTree = ""; }; - 3D2412E620BAEB56005A04D6 /* .jazzy.yaml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .jazzy.yaml; sourceTree = SOURCE_ROOT; }; - 3D2412E720BAEB56005A04D6 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; - 3D2412E920BAEB56005A04D6 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = SOURCE_ROOT; }; - 3D2412EA20BAEB56005A04D6 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; - 3D2412EB20BAEB56005A04D6 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; - 3D2412EC20BAEB56005A04D6 /* WhatsNewKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = WhatsNewKit.podspec; sourceTree = SOURCE_ROOT; }; - 3D2803CB220598F400E1FA31 /* Anchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Anchor.swift; sourceTree = ""; }; - 3D2803CE2205A1B800E1FA31 /* WhatsNewTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewTitleViewController.swift; sourceTree = ""; }; - 3D2803D42205A6B600E1FA31 /* WhatsNewButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewButtonViewController.swift; sourceTree = ""; }; - 3D2803D62205AA7F00E1FA31 /* WhatsNewButtonViewController+DetailButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewButtonViewController+DetailButton.swift"; sourceTree = ""; }; - 3D2803D82205ABAB00E1FA31 /* WhatsNewButtonViewController+CompletionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewButtonViewController+CompletionButton.swift"; sourceTree = ""; }; - 3D2803DA2205AE5C00E1FA31 /* WhatsNewItemsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewItemsViewController.swift; sourceTree = ""; }; - 3D2803DC2205B4E200E1FA31 /* WhatsNewItemsViewController+Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewItemsViewController+Cell.swift"; sourceTree = ""; }; - 3D2803E52205D4E800E1FA31 /* VersionStoreConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionStoreConfiguration.swift; sourceTree = ""; }; - 3D299143217B5D8100132C91 /* ExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = ""; }; - 3D299145217B5EDD00132C91 /* ConfigurationsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationsTableViewCell.swift; sourceTree = ""; }; - 3D299147217B5FC700132C91 /* ConfigurationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationCollectionViewCell.swift; sourceTree = ""; }; - 3D299149217B691100132C91 /* ExampleViewController+PresentWhatsNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExampleViewController+PresentWhatsNew.swift"; sourceTree = ""; }; - 3D29914D217B6E7100132C91 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - 3D29914F217B6E8500132C91 /* BackgroundColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundColorConfiguration.swift; sourceTree = ""; }; - 3D299151217B6E9600132C91 /* TintColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintColorConfiguration.swift; sourceTree = ""; }; - 3D299153217B6EA600132C91 /* AnimationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationConfiguration.swift; sourceTree = ""; }; - 3D299155217B6EBA00132C91 /* LayoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutConfiguration.swift; sourceTree = ""; }; - 3D29915C217B6FCD00132C91 /* Collection+Safe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Safe.swift"; sourceTree = ""; }; - 3D29915E217B720400132C91 /* ReuseIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReuseIdentifiable.swift; sourceTree = ""; }; - 3D299160217B7EEB00132C91 /* UIImage+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Assets.swift"; sourceTree = ""; }; - 3D330EC3217B8E8E002359E7 /* HapticFeedbackConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedbackConfiguration.swift; sourceTree = ""; }; - 3D33221C21C416E900B2949F /* SecondaryTitleColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTitleColorConfiguration.swift; sourceTree = ""; }; - 3D375B9F20BC6582005DE31B /* UIColor+Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Main.swift"; sourceTree = ""; }; - 3D562CB820B3FD3200603182 /* WhatsNew+Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNew+Version.swift"; sourceTree = ""; }; - 3D5AA3BC252078C8002F6467 /* Disposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disposable.swift; sourceTree = ""; }; - 3D5AA3BF252078E6002F6467 /* DisposeBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBag.swift; sourceTree = ""; }; - 3D5D06BC20C7C9C700EC7C46 /* WhatsNewViewController+TitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+TitleView.swift"; sourceTree = ""; }; - 3D5D06BE20C7CA2700EC7C46 /* WhatsNewViewController+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+Animation.swift"; sourceTree = ""; }; - 3D5D06C020C7CAEC00EC7C46 /* WhatsNewViewController+ItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+ItemsView.swift"; sourceTree = ""; }; - 3D7E00C320B6A5F10036BDB1 /* WhatsNewViewController+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+Configuration.swift"; sourceTree = ""; }; - 3D914522237DC191006EAF1A /* main.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; name = main.yml; path = .github/workflows/main.yml; sourceTree = SOURCE_ROOT; }; - 3D92C66920AF87E600A10333 /* WhatsNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNew.swift; sourceTree = ""; }; - 3D92C66D20AF87F500A10333 /* WhatsNew+Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNew+Item.swift"; sourceTree = ""; }; - 3D92C67020AF883300A10333 /* WhatsNewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewViewController.swift; sourceTree = ""; }; - 3DAEF0172328469B00A61D87 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; - 3DB507B824420E6C0014A479 /* UIView+MakeConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+MakeConstraints.swift"; sourceTree = ""; }; - 3DBE9414236D7A480026B0FB /* WhatsNewViewController+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WhatsNewViewController+Theme.swift"; sourceTree = ""; }; - 3DBFB67520B6EBC100FF0899 /* InMemoryWhatsNewVersionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryWhatsNewVersionStore.swift; sourceTree = ""; }; - 3DC8A6A820AED02400292C94 /* WhatsNewKit-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WhatsNewKit-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3DC8A6AA20AED02400292C94 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 3DC8A6B120AED02600292C94 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 3DC8A6B420AED02600292C94 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 3DC8A6B620AED02600292C94 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3DDDB59620B832AB001D59C5 /* UIColor+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Defaults.swift"; sourceTree = ""; }; - 3DF12C2A2210AE41006473F0 /* ContentModeConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentModeConfiguration.swift; sourceTree = ""; }; - 3DF12C2C2210AF51006473F0 /* ItemsCountConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsCountConfiguration.swift; sourceTree = ""; }; - 3DFB6FFF20B34F8800673FD6 /* WhatsNewVersionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewVersionStore.swift; sourceTree = ""; }; - 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WhatsNewKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 52D6D9861BEFF229002C0205 /* WhatsNewKit-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "WhatsNewKit-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - AD2FAA261CD0B6D800659CF4 /* WhatsNewKit.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = WhatsNewKit.plist; sourceTree = ""; }; - AD2FAA281CD0B6E100659CF4 /* WhatsNewKitTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = WhatsNewKitTests.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 3DC8A6A520AED02400292C94 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 3DA6679C20B0BB3B00DB18FF /* WhatsNewKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D9781BEFF229002C0205 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D9831BEFF229002C0205 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 52D6D9871BEFF229002C0205 /* WhatsNewKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 3D04214A20C2D6A100DA5126 /* Models */ = { - isa = PBXGroup; - children = ( - 3D04214B20C2D6A100DA5126 /* WhatsNewVersionTests.swift */, - 3D04214C20C2D6A100DA5126 /* WhatsNewItemTests.swift */, - 3D04214D20C2D6A100DA5126 /* WhatsNewTests.swift */, - ); - path = Models; - sourceTree = ""; - }; - 3D04214E20C2D6A100DA5126 /* Stores */ = { - isa = PBXGroup; - children = ( - 3D04214F20C2D6A100DA5126 /* KeyValueWhatsNewVersionStoreTests.swift */, - 3D04215020C2D6A100DA5126 /* InMemoryWhatsNewVersionStoreTests.swift */, - ); - path = Stores; - sourceTree = ""; - }; - 3D29914B217B6E2000132C91 /* ExampleViewController */ = { - isa = PBXGroup; - children = ( - 3D299143217B5D8100132C91 /* ExampleViewController.swift */, - 3D299149217B691100132C91 /* ExampleViewController+PresentWhatsNew.swift */, - 3D299145217B5EDD00132C91 /* ConfigurationsTableViewCell.swift */, - 3D299147217B5FC700132C91 /* ConfigurationCollectionViewCell.swift */, - ); - path = ExampleViewController; - sourceTree = ""; - }; - 3D29914C217B6E6200132C91 /* Configurations */ = { - isa = PBXGroup; - children = ( - 3D29914D217B6E7100132C91 /* Configuration.swift */, - 3D29914F217B6E8500132C91 /* BackgroundColorConfiguration.swift */, - 3D299151217B6E9600132C91 /* TintColorConfiguration.swift */, - 3D299153217B6EA600132C91 /* AnimationConfiguration.swift */, - 3D2803E52205D4E800E1FA31 /* VersionStoreConfiguration.swift */, - 3D299155217B6EBA00132C91 /* LayoutConfiguration.swift */, - 3DF12C2A2210AE41006473F0 /* ContentModeConfiguration.swift */, - 3D330EC3217B8E8E002359E7 /* HapticFeedbackConfiguration.swift */, - 3D33221C21C416E900B2949F /* SecondaryTitleColorConfiguration.swift */, - 2264FE182441B2630030A4CE /* TitleModeConfiguration.swift */, - 3DF12C2C2210AF51006473F0 /* ItemsCountConfiguration.swift */, - ); - path = Configurations; - sourceTree = ""; - }; - 3D299157217B6EDF00132C91 /* Shared */ = { - isa = PBXGroup; - children = ( - 3D299160217B7EEB00132C91 /* UIImage+Assets.swift */, - 3D375B9F20BC6582005DE31B /* UIColor+Main.swift */, - 3D29915C217B6FCD00132C91 /* Collection+Safe.swift */, - 3D29915E217B720400132C91 /* ReuseIdentifiable.swift */, - ); - path = Shared; - sourceTree = ""; - }; - 3D5AA3BB252078BE002F6467 /* DataStructures */ = { - isa = PBXGroup; - children = ( - 3D5AA3BE252078DB002F6467 /* Disposable */, - ); - path = DataStructures; - sourceTree = ""; - }; - 3D5AA3BE252078DB002F6467 /* Disposable */ = { - isa = PBXGroup; - children = ( - 3D5AA3BC252078C8002F6467 /* Disposable.swift */, - 3D5AA3BF252078E6002F6467 /* DisposeBag.swift */, - ); - path = Disposable; - sourceTree = ""; - }; - 3D92C66C20AF87EA00A10333 /* Models */ = { - isa = PBXGroup; - children = ( - 3D92C66920AF87E600A10333 /* WhatsNew.swift */, - 3D562CB820B3FD3200603182 /* WhatsNew+Version.swift */, - 3D92C66D20AF87F500A10333 /* WhatsNew+Item.swift */, - ); - path = Models; - sourceTree = ""; - }; - 3D92C67920AF88DC00A10333 /* Components */ = { - isa = PBXGroup; - children = ( - 3DA1829C20B336BF00D2C155 /* Title */, - 3DA1829D20B336C600D2C155 /* Items */, - 3DA1829E20B336CB00D2C155 /* Buttons */, - ); - path = Components; - sourceTree = ""; - }; - 3D92C67A20AF88E000A10333 /* Configuration */ = { - isa = PBXGroup; - children = ( - 3D7E00C320B6A5F10036BDB1 /* WhatsNewViewController+Configuration.swift */, - 3D5D06BC20C7C9C700EC7C46 /* WhatsNewViewController+TitleView.swift */, - 3D5D06C020C7CAEC00EC7C46 /* WhatsNewViewController+ItemsView.swift */, - 3D2412CE20BAE75E005A04D6 /* WhatsNewViewController+DetailButton.swift */, - 3D2412D020BAE7C6005A04D6 /* WhatsNewViewController+CompletionButton.swift */, - 3D13455D20BC8AFA00BE0FF8 /* WhatsNewViewController+HapticFeedback.swift */, - 3D5D06BE20C7CA2700EC7C46 /* WhatsNewViewController+Animation.swift */, - ); - path = Configuration; - sourceTree = ""; - }; - 3DA1829C20B336BF00D2C155 /* Title */ = { - isa = PBXGroup; - children = ( - 3D2803CE2205A1B800E1FA31 /* WhatsNewTitleViewController.swift */, - ); - path = Title; - sourceTree = ""; - }; - 3DA1829D20B336C600D2C155 /* Items */ = { - isa = PBXGroup; - children = ( - 3D2803DA2205AE5C00E1FA31 /* WhatsNewItemsViewController.swift */, - 3D2803DC2205B4E200E1FA31 /* WhatsNewItemsViewController+Cell.swift */, - ); - path = Items; - sourceTree = ""; - }; - 3DA1829E20B336CB00D2C155 /* Buttons */ = { - isa = PBXGroup; - children = ( - 3D2803D42205A6B600E1FA31 /* WhatsNewButtonViewController.swift */, - 3D2803D62205AA7F00E1FA31 /* WhatsNewButtonViewController+DetailButton.swift */, - 3D2803D82205ABAB00E1FA31 /* WhatsNewButtonViewController+CompletionButton.swift */, - ); - path = Buttons; - sourceTree = ""; - }; - 3DBE9413236D7A380026B0FB /* Theme */ = { - isa = PBXGroup; - children = ( - 3DBE9414236D7A480026B0FB /* WhatsNewViewController+Theme.swift */, - ); - path = Theme; - sourceTree = ""; - }; - 3DBFB67720B6ED9300FF0899 /* Resources */ = { - isa = PBXGroup; - children = ( - 3DC8A6B120AED02600292C94 /* Assets.xcassets */, - 3DC8A6B320AED02600292C94 /* LaunchScreen.storyboard */, - 3DC8A6B620AED02600292C94 /* Info.plist */, - ); - path = Resources; - sourceTree = ""; - }; - 3DC8A6A920AED02400292C94 /* Example */ = { - isa = PBXGroup; - children = ( - 3DC8A6AA20AED02400292C94 /* AppDelegate.swift */, - 3D29914B217B6E2000132C91 /* ExampleViewController */, - 3D29914C217B6E6200132C91 /* Configurations */, - 3D299157217B6EDF00132C91 /* Shared */, - 3DBFB67720B6ED9300FF0899 /* Resources */, - ); - path = Example; - sourceTree = ""; - }; - 3DC8A6BA20AED37B00292C94 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 3DDDB59520B83296001D59C5 /* Extensions */ = { - isa = PBXGroup; - children = ( - 3D2803CB220598F400E1FA31 /* Anchor.swift */, - 3DDDB59620B832AB001D59C5 /* UIColor+Defaults.swift */, - 3DB507B824420E6C0014A479 /* UIView+MakeConstraints.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 3DFB6FFE20B34F7E00673FD6 /* Store */ = { - isa = PBXGroup; - children = ( - 3DFB6FFF20B34F8800673FD6 /* WhatsNewVersionStore.swift */, - 3D2412C620BADBDC005A04D6 /* KeyValueWhatsNewVersionStore.swift */, - 3DBFB67520B6EBC100FF0899 /* InMemoryWhatsNewVersionStore.swift */, - ); - path = Store; - sourceTree = ""; - }; - 52D6D9721BEFF229002C0205 = { - isa = PBXGroup; - children = ( - 8933C7811EB5B7E0000D00A4 /* Sources */, - 8933C7831EB5B7EB000D00A4 /* Tests */, - 3DC8A6A920AED02400292C94 /* Example */, - 52D6D99C1BEFF38C002C0205 /* Configs */, - 52D6D97D1BEFF229002C0205 /* Products */, - 3DC8A6BA20AED37B00292C94 /* Frameworks */, - ); - sourceTree = ""; - }; - 52D6D97D1BEFF229002C0205 /* Products */ = { - isa = PBXGroup; - children = ( - 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */, - 52D6D9861BEFF229002C0205 /* WhatsNewKit-iOS Tests.xctest */, - 3DC8A6A820AED02400292C94 /* WhatsNewKit-Example.app */, - ); - name = Products; - sourceTree = ""; - }; - 52D6D99C1BEFF38C002C0205 /* Configs */ = { - isa = PBXGroup; - children = ( - DD7502721C68FC1B006590AF /* Frameworks */, - DD7502731C68FC20006590AF /* Tests */, - 3D2412E920BAEB56005A04D6 /* .gitignore */, - 3D914522237DC191006EAF1A /* main.yml */, - 3D2412E620BAEB56005A04D6 /* .jazzy.yaml */, - 3D2412EB20BAEB56005A04D6 /* .swiftlint.yml */, - 3D2412E720BAEB56005A04D6 /* LICENSE */, - 3D2412EA20BAEB56005A04D6 /* README.md */, - 3DAEF0172328469B00A61D87 /* Package.swift */, - 3D2412EC20BAEB56005A04D6 /* WhatsNewKit.podspec */, - ); - path = Configs; - sourceTree = ""; - }; - 8933C7811EB5B7E0000D00A4 /* Sources */ = { - isa = PBXGroup; - children = ( - 3D92C67020AF883300A10333 /* WhatsNewViewController.swift */, - 3D92C67A20AF88E000A10333 /* Configuration */, - 3DBE9413236D7A380026B0FB /* Theme */, - 3DFB6FFE20B34F7E00673FD6 /* Store */, - 3D92C67920AF88DC00A10333 /* Components */, - 3D92C66C20AF87EA00A10333 /* Models */, - 3D5AA3BB252078BE002F6467 /* DataStructures */, - 3DDDB59520B83296001D59C5 /* Extensions */, - ); - path = Sources; - sourceTree = ""; - }; - 8933C7831EB5B7EB000D00A4 /* Tests */ = { - isa = PBXGroup; - children = ( - 3D04214520C2D68C00DA5126 /* BaseTests.swift */, - 3D04214720C2D6A000DA5126 /* WhatsNewViewControllerTests.swift */, - 3D04214A20C2D6A100DA5126 /* Models */, - 3D04214E20C2D6A100DA5126 /* Stores */, - ); - path = Tests; - sourceTree = ""; - }; - DD7502721C68FC1B006590AF /* Frameworks */ = { - isa = PBXGroup; - children = ( - AD2FAA261CD0B6D800659CF4 /* WhatsNewKit.plist */, - ); - name = Frameworks; - sourceTree = ""; - }; - DD7502731C68FC20006590AF /* Tests */ = { - isa = PBXGroup; - children = ( - AD2FAA281CD0B6E100659CF4 /* WhatsNewKitTests.plist */, - ); - name = Tests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 52D6D9791BEFF229002C0205 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 3DC8A6A720AED02400292C94 /* WhatsNewKit-Example */ = { - isa = PBXNativeTarget; - buildConfigurationList = 3DC8A6B720AED02600292C94 /* Build configuration list for PBXNativeTarget "WhatsNewKit-Example" */; - buildPhases = ( - 3DC8A6A420AED02400292C94 /* Sources */, - 3DC8A6A520AED02400292C94 /* Frameworks */, - 3DC8A6A620AED02400292C94 /* Resources */, - 3DA667A020B0BB3B00DB18FF /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 3DA6679F20B0BB3B00DB18FF /* PBXTargetDependency */, - ); - name = "WhatsNewKit-Example"; - productName = Example; - productReference = 3DC8A6A820AED02400292C94 /* WhatsNewKit-Example.app */; - productType = "com.apple.product-type.application"; - }; - 52D6D97B1BEFF229002C0205 /* WhatsNewKit-iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "WhatsNewKit-iOS" */; - buildPhases = ( - 52D6D9771BEFF229002C0205 /* Sources */, - 52D6D9781BEFF229002C0205 /* Frameworks */, - 52D6D9791BEFF229002C0205 /* Headers */, - 52D6D97A1BEFF229002C0205 /* Resources */, - 3DFB700320B3591E00673FD6 /* SwiftLint */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "WhatsNewKit-iOS"; - productName = WhatsNewKit; - productReference = 52D6D97C1BEFF229002C0205 /* WhatsNewKit.framework */; - productType = "com.apple.product-type.framework"; - }; - 52D6D9851BEFF229002C0205 /* WhatsNewKit-iOS Tests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "WhatsNewKit-iOS Tests" */; - buildPhases = ( - 52D6D9821BEFF229002C0205 /* Sources */, - 52D6D9831BEFF229002C0205 /* Frameworks */, - 52D6D9841BEFF229002C0205 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 52D6D9891BEFF229002C0205 /* PBXTargetDependency */, - ); - name = "WhatsNewKit-iOS Tests"; - productName = WhatsNewKitTests; - productReference = 52D6D9861BEFF229002C0205 /* WhatsNewKit-iOS Tests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 52D6D9731BEFF229002C0205 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0930; - LastUpgradeCheck = 1200; - ORGANIZATIONNAME = WhatsNewKit; - TargetAttributes = { - 3DC8A6A720AED02400292C94 = { - CreatedOnToolsVersion = 9.3.1; - ProvisioningStyle = Automatic; - }; - 52D6D97B1BEFF229002C0205 = { - CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 0800; - }; - 52D6D9851BEFF229002C0205 = { - CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 0940; - }; - }; - }; - buildConfigurationList = 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "WhatsNewKit" */; - compatibilityVersion = "Xcode 6.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 52D6D9721BEFF229002C0205; - productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 52D6D97B1BEFF229002C0205 /* WhatsNewKit-iOS */, - 52D6D9851BEFF229002C0205 /* WhatsNewKit-iOS Tests */, - 3DC8A6A720AED02400292C94 /* WhatsNewKit-Example */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 3DC8A6A620AED02400292C94 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3DC8A6B520AED02600292C94 /* LaunchScreen.storyboard in Resources */, - 3DC8A6B220AED02600292C94 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D97A1BEFF229002C0205 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D9841BEFF229002C0205 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3DFB700320B3591E00673FD6 /* SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = SwiftLint; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 3DC8A6A420AED02400292C94 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3D299148217B5FC700132C91 /* ConfigurationCollectionViewCell.swift in Sources */, - 3D2803E62205D4E800E1FA31 /* VersionStoreConfiguration.swift in Sources */, - 3D29915F217B720400132C91 /* ReuseIdentifiable.swift in Sources */, - 3D29914A217B691100132C91 /* ExampleViewController+PresentWhatsNew.swift in Sources */, - 3DC8A6AB20AED02400292C94 /* AppDelegate.swift in Sources */, - 3D299152217B6E9600132C91 /* TintColorConfiguration.swift in Sources */, - 2264FE192441B2630030A4CE /* TitleModeConfiguration.swift in Sources */, - 3D375BA020BC6582005DE31B /* UIColor+Main.swift in Sources */, - 3D299150217B6E8500132C91 /* BackgroundColorConfiguration.swift in Sources */, - 3D299146217B5EDD00132C91 /* ConfigurationsTableViewCell.swift in Sources */, - 3D29914E217B6E7100132C91 /* Configuration.swift in Sources */, - 3D299154217B6EA600132C91 /* AnimationConfiguration.swift in Sources */, - 3D33221D21C416E900B2949F /* SecondaryTitleColorConfiguration.swift in Sources */, - 3D299156217B6EBA00132C91 /* LayoutConfiguration.swift in Sources */, - 3D29915D217B6FCD00132C91 /* Collection+Safe.swift in Sources */, - 3D299161217B7EEB00132C91 /* UIImage+Assets.swift in Sources */, - 3D299144217B5D8100132C91 /* ExampleViewController.swift in Sources */, - 3DF12C2D2210AF51006473F0 /* ItemsCountConfiguration.swift in Sources */, - 3D330EC4217B8E8E002359E7 /* HapticFeedbackConfiguration.swift in Sources */, - 3DF12C2B2210AE41006473F0 /* ContentModeConfiguration.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D9771BEFF229002C0205 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3D2803DB2205AE5C00E1FA31 /* WhatsNewItemsViewController.swift in Sources */, - 3D7E00C420B6A5F10036BDB1 /* WhatsNewViewController+Configuration.swift in Sources */, - 3D5AA3C0252078E6002F6467 /* DisposeBag.swift in Sources */, - 3DBE9416236D7A6E0026B0FB /* WhatsNewViewController+Theme.swift in Sources */, - 3D5D06BD20C7C9C700EC7C46 /* WhatsNewViewController+TitleView.swift in Sources */, - 3D2803D02205A1F500E1FA31 /* WhatsNewTitleViewController.swift in Sources */, - 3D2803D72205AA7F00E1FA31 /* WhatsNewButtonViewController+DetailButton.swift in Sources */, - 3D5AA3BD252078C8002F6467 /* Disposable.swift in Sources */, - 3D2803CD22059B2200E1FA31 /* Anchor.swift in Sources */, - 3DDDB59720B832AB001D59C5 /* UIColor+Defaults.swift in Sources */, - 3D2803DD2205B4E200E1FA31 /* WhatsNewItemsViewController+Cell.swift in Sources */, - 3D92C66A20AF87E600A10333 /* WhatsNew.swift in Sources */, - 3DFB700020B34F8800673FD6 /* WhatsNewVersionStore.swift in Sources */, - 3DBFB67620B6EBC100FF0899 /* InMemoryWhatsNewVersionStore.swift in Sources */, - 3D13455E20BC8AFA00BE0FF8 /* WhatsNewViewController+HapticFeedback.swift in Sources */, - 3D92C67120AF883300A10333 /* WhatsNewViewController.swift in Sources */, - 3D2412CF20BAE75E005A04D6 /* WhatsNewViewController+DetailButton.swift in Sources */, - 3D5D06C120C7CAEC00EC7C46 /* WhatsNewViewController+ItemsView.swift in Sources */, - 3D92C66E20AF87F500A10333 /* WhatsNew+Item.swift in Sources */, - 3D2412C720BADBDC005A04D6 /* KeyValueWhatsNewVersionStore.swift in Sources */, - 3D2803D92205ABAB00E1FA31 /* WhatsNewButtonViewController+CompletionButton.swift in Sources */, - 3D5D06BF20C7CA2700EC7C46 /* WhatsNewViewController+Animation.swift in Sources */, - 3D2803D52205A6B600E1FA31 /* WhatsNewButtonViewController.swift in Sources */, - 3DB507B924420E6C0014A479 /* UIView+MakeConstraints.swift in Sources */, - 3D562CB920B3FD3200603182 /* WhatsNew+Version.swift in Sources */, - 3D2412D120BAE7C6005A04D6 /* WhatsNewViewController+CompletionButton.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 52D6D9821BEFF229002C0205 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3D04215620C2D6A100DA5126 /* KeyValueWhatsNewVersionStoreTests.swift in Sources */, - 3D04215720C2D6A100DA5126 /* InMemoryWhatsNewVersionStoreTests.swift in Sources */, - 3D04215520C2D6A100DA5126 /* WhatsNewTests.swift in Sources */, - 3D04214620C2D68C00DA5126 /* BaseTests.swift in Sources */, - 3D04215120C2D6A100DA5126 /* WhatsNewViewControllerTests.swift in Sources */, - 3D04215320C2D6A100DA5126 /* WhatsNewVersionTests.swift in Sources */, - 3D04215420C2D6A100DA5126 /* WhatsNewItemTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 3DA6679F20B0BB3B00DB18FF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 52D6D97B1BEFF229002C0205 /* WhatsNewKit-iOS */; - targetProxy = 3DA6679E20B0BB3B00DB18FF /* PBXContainerItemProxy */; - }; - 52D6D9891BEFF229002C0205 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 52D6D97B1BEFF229002C0205 /* WhatsNewKit-iOS */; - targetProxy = 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 3DC8A6B320AED02600292C94 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 3DC8A6B420AED02600292C94 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 3DC8A6B820AED02600292C94 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "$(SRCROOT)/Example/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = de.tiigi.WhatsNewKit.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 3DC8A6B920AED02600292C94 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "$(SRCROOT)/Example/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_LDFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = de.tiigi.WhatsNewKit.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 52D6D98E1BEFF229002C0205 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 52D6D98F1BEFF229002C0205 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 52D6D9911BEFF229002C0205 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Configs/WhatsNewKit.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ""; - OTHER_SWIFT_FLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.WhatsNewKit-iOS"; - PRODUCT_NAME = WhatsNewKit; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 52D6D9921BEFF229002C0205 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Configs/WhatsNewKit.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_LDFLAGS = ""; - OTHER_SWIFT_FLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = "de.tiigi.WhatsNewKit.WhatsNewKit-iOS"; - PRODUCT_NAME = WhatsNewKit; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 52D6D9941BEFF229002C0205 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - INFOPLIST_FILE = Configs/WhatsNewKitTests.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.WhatsNewKit.WhatsNewKit-iOS-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 52D6D9951BEFF229002C0205 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - INFOPLIST_FILE = Configs/WhatsNewKitTests.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.WhatsNewKit.WhatsNewKit-iOS-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 3DC8A6B720AED02600292C94 /* Build configuration list for PBXNativeTarget "WhatsNewKit-Example" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 3DC8A6B820AED02600292C94 /* Debug */, - 3DC8A6B920AED02600292C94 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "WhatsNewKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 52D6D98E1BEFF229002C0205 /* Debug */, - 52D6D98F1BEFF229002C0205 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "WhatsNewKit-iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 52D6D9911BEFF229002C0205 /* Debug */, - 52D6D9921BEFF229002C0205 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "WhatsNewKit-iOS Tests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 52D6D9941BEFF229002C0205 /* Debug */, - 52D6D9951BEFF229002C0205 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 52D6D9731BEFF229002C0205 /* Project object */; -} diff --git a/fastlane/.env b/fastlane/.env deleted file mode 100644 index ef9bcc7..0000000 --- a/fastlane/.env +++ /dev/null @@ -1 +0,0 @@ -FASTLANE_SKIP_UPDATE_CHECK=1 diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index b6a8ff8..0000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,86 +0,0 @@ -fastlane_version "2.120.0" - -default_platform :ios - -platform :ios do - - desc "Release a new version of WhatsNewKit" - lane :release do |options| - # Ensure Git status is clean - ensure_git_status_clean - # Ensure Git branch is master - ensure_git_branch(branch: 'master') - # Perform Dependency-Manager compatibility tests - compatibilityTests - # Perform Tests - tests - # Retrieve Version from options - version = options[:version] - # Increment Version - increment(version: version) - # Add Git Tag - add_git_tag(tag: version) - # Push Git Tag - push_git_tags() - # Push Git commit - push_to_git_remote() - # Pod push / Pod trunk - pod_push() - end - - desc "Increment Version" - lane :increment do |options| - # Retrieve Version from options - version = options[:version] - # Set Podspec version - version_bump_podspec( - path: "WhatsNewKit.podspec", - version_number: version - ) - # Set Framework plist version - set_info_plist_value( - path: "Configs/WhatsNewKit.plist", - key: "CFBundleShortVersionString", - value: version - ) - # Set Framework Tests plist version - set_info_plist_value( - path: "Configs/WhatsNewKitTests.plist", - key: "CFBundleShortVersionString", - value: version - ) - # Set Example plist version - set_info_plist_value( - path: "Example/Resources/Info.plist", - key: "CFBundleShortVersionString", - value: version - ) - # Commit modified files - git_commit( - path: [ - "WhatsNewKit.podspec", - "Configs/WhatsNewKit.plist", - "Configs/WhatsNewKitTests.plist", - "Example/Resources/Info.plist" - ], - message: "WhatsNewKit Version #{version} 🚀" - ) - end - - desc "Runs tests" - lane :tests do - # Perform iOS Tests - scan( - project: "WhatsNewKit.xcodeproj", - scheme: "WhatsNewKit-iOS", - clean: true - ) - end - - desc "Run Dependency-Manager compatibility tests" - lane :compatibilityTests do - # Pod lib lint to ensure CocoaPods compatibility - pod_lib_lint(allow_warnings: true) - end - -end