diff --git a/Sample/Sample/UI/NewFeatureOnboardingSheetView.swift b/Sample/Sample/UI/NewFeatureOnboardingSheetView.swift index 52401fd0f..8b8fd31dc 100644 --- a/Sample/Sample/UI/NewFeatureOnboardingSheetView.swift +++ b/Sample/Sample/UI/NewFeatureOnboardingSheetView.swift @@ -9,8 +9,6 @@ import SwiftUI import OnboardingUI struct NewFeatureOnboardingSheetView: View { - @Environment(\.dismiss) private var dismiss - var action: () -> Void init(action: @escaping () -> Void) { @@ -19,33 +17,31 @@ struct NewFeatureOnboardingSheetView: View { var body: some View { OnboardingSheetView { - OnboardingTitle("What's New in\nOnboardingUI") + Text("What's New in\nOnboardingUI") + .onboardingTextFormatting(style: .title) } content: { - OnboardingItem(systemName: "tree",shape: .green) { - OnboardingSubtitle("New AppVersionManager environment variable") - OnboardingContent("The new AppVersionManager environment variable allows you to display onboarding at the intended time.") + OnboardingItem(systemName: "wrench.and.screwdriver",shape: .red) { + Text("New AppVersionManager environment variable") + .onboardingTextFormatting(style: .subtitle) + Text("The new AppVersionManager environment variable allows you to display onboarding at the intended time.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "building.columns",shape: .blue) { - OnboardingSubtitle("New Onboarding protocol and Feature structure") - OnboardingContent("The new Onboarding protocol and Feature structure make it easier to create onboarding. There is no need to build views.") + Text("New Onboarding protocol and Feature structure") + .onboardingTextFormatting(style: .subtitle) + Text("The new Onboarding protocol and Feature structure make it easier to create onboarding. There is no need to build views.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "wrench.and.screwdriver",shape: .orange) { - OnboardingSubtitle("Customize the look and feel") - OnboardingContent("Of course, it is also customizable. You can build onboarding at will.") - } - -#if os(tvOS) - OnboardingItem(systemName: "ellipsis",shape: .white) { - OnboardingSubtitle("Many other benefits") - OnboardingContent("Now, tvOS is also supported, making it easy to create onboarding. Now you can create onboarding for all platforms except watchOS.") + Text("Customize the look and feel") + .onboardingTextFormatting(style: .subtitle) + Text("Of course, it is also customizable. You can build onboarding at will.") + .onboardingTextFormatting(style: .content) } -#endif } button: { - ContinueButton(color: .accentColor, action: { - dismiss() - }) + ContinueButton(color: .accentColor, action: action) } } } diff --git a/Sample/Sample/UI/WelcomeOnboardingSheetView.swift b/Sample/Sample/UI/WelcomeOnboardingSheetView.swift index 7d174bdd1..42620fecf 100644 --- a/Sample/Sample/UI/WelcomeOnboardingSheetView.swift +++ b/Sample/Sample/UI/WelcomeOnboardingSheetView.swift @@ -20,21 +20,28 @@ struct WelcomeOnboardingSheetView: View { var body: some View { OnboardingSheetView { - OnboardingTitle("Welcome to\nOnboardingUI") + Text("Welcome to\nOnboardingUI") + .onboardingTextFormatting(style: .title) } content: { OnboardingItem(systemName: "applescript",shape: .red) { - OnboardingSubtitle("Easy to Make") - OnboardingContent("Onboarding screens like Apple's stock apps can be easily created with SwiftUI.") + Text("Easy to Make") + .onboardingTextFormatting(style: .subtitle) + Text("Onboarding screens like Apple's stock apps can be easily created with SwiftUI.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "apple.logo") { - OnboardingSubtitle("Not only for iPhone, but also for Mac, iPad, Vision Pro") - OnboardingContent("It supports not only iPhone, but also Mac, iPad, and Vision Pro. Therefore, there is no need to rewrite the code for each device.") + Text("Not only for iPhone, but also for Mac, iPad, Vision Pro") + .onboardingTextFormatting(style: .subtitle) + Text("It supports not only iPhone, but also Mac, iPad, and Vision Pro. Therefore, there is no need to rewrite the code for each device.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "circle.badge.checkmark",mode: .palette,primary: .primary,secondary: .blue) { - OnboardingSubtitle("Customize SF Symbols") - OnboardingContent("When using a highly customizable implementation method, multi-color and SF symbol hierarchies are supported and can be freely customized.") + Text("Customize SF Symbols") + .onboardingTextFormatting(style: .subtitle) + Text("When using a highly customizable implementation method, multi-color and SF symbol hierarchies are supported and can be freely customized.") + .onboardingTextFormatting(style: .content) } #if os(tvOS) diff --git a/Sources/OnboardingUI/Processing/AppVersionManager.swift b/Sources/OnboardingUI/Processing/AppVersionManager.swift index b78617da3..80f1a0009 100644 --- a/Sources/OnboardingUI/Processing/AppVersionManager.swift +++ b/Sources/OnboardingUI/Processing/AppVersionManager.swift @@ -22,8 +22,8 @@ public class AppVersionManager { userDefaults.set(lastOpenedVersion, forKey: "LastOpenedVersion") } } - /// Whether or not this is the first activation - public var isTheFirstLaunch: Bool { + /// Whether or not this is the first activation. + public var isTheFirstActivation: Bool { get { return lastOpenedVersion == "" } @@ -35,13 +35,9 @@ public class AppVersionManager { /// Variable to detect if the major version number has increased. public var isMajorVersionUpdated: Bool { get { - if !lastOpenedVersion.isEmpty { - let lastOpenedComponents = parseVersion(lastOpenedVersion) - let currentComponents = parseVersion(version) - return lastOpenedComponents.major < currentComponents.major - } else { - return false - } + let lastOpenedComponents = filled(splitByDot(lastOpenedVersion), count: 3) + let currentComponents = filled(splitByDot(version), count: 3) + return lastOpenedComponents[0] < currentComponents[0] } set { @@ -49,15 +45,13 @@ public class AppVersionManager { } } /// Variable to detect if the minor version number or higher has increased. - public var isMinorVersionUpdated: Bool { + public var isMinorOrPatchVersionUpdated: Bool { get { - if !lastOpenedVersion.isEmpty { - let lastOpenedComponents = parseVersion(lastOpenedVersion) - let currentComponents = parseVersion(version) - return lastOpenedComponents.major == currentComponents.major && lastOpenedComponents.minor < currentComponents.minor - } else { - return false - } + let lastOpenedComponents = filled(splitByDot(lastOpenedVersion), count: 3) + let currentComponents = filled(splitByDot(version), count: 3) + return lastOpenedComponents[0] == currentComponents[0] && + (lastOpenedComponents[1] < currentComponents[1] || + (lastOpenedComponents[1] == currentComponents[1] && lastOpenedComponents[2] < currentComponents[2])) } set { @@ -65,23 +59,38 @@ public class AppVersionManager { } } /// Default initializer + /// Creates a new AppVersionManager instance. public init() { - self.version = Bundle.main - .object( - forInfoDictionaryKey: "CFBundleShortVersionString" - ) as! String - self.lastOpenedVersion = userDefaults - .string(forKey: "LastOpenedVersion") ?? "" - } - - func parseVersion(_ versionString: String) -> (major: Int, minor: Int, patch: Int) { - var components = versionString.split(separator: ".").compactMap { Int($0) } - components = (0..<3).map { $0 < components.count ? components[$0] : 0 } - return (major: components[0], minor: components[1], patch: components[2]) + self.version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + self.lastOpenedVersion = userDefaults.string(forKey: "LastOpenedVersion") ?? "" } } -/// AppVersionManager environment values +/// AppVersionManager environment key +/// The environment key for the AppVersionManager. @available(iOS 17.0,macOS 14.0,watchOS 10.0,tvOS 17.0,visionOS 1.0,*) +public struct AppVersionManagerKey: EnvironmentKey { + public static var defaultValue = AppVersionManager() +} +/// AppVersionManager environment values public extension EnvironmentValues { - @Entry var appVersionManager: AppVersionManager = AppVersionManager() + /// Accessor for the AppVersionManager value in EnvironmentValues. + var appVersionManager: AppVersionManager { + get { self[AppVersionManagerKey.self] } + set { self[AppVersionManagerKey.self] = newValue } + } } +/// Function to split the version number dot by dot +@available(iOS 17.0,macOS 14.0,watchOS 10.0,tvOS 17.0,visionOS 1.0,*) +func splitByDot(_ versionNumber: String) -> [Int] { + return versionNumber.split(separator: ".").map { string -> Int in + return Int(string) ?? 0 + } +} +/// Function to unify the number of elements in an array +@available(iOS 17.0,macOS 14.0,watchOS 10.0,tvOS 17.0,visionOS 1.0,*) +func filled(_ target: [Int], count: Int) -> [Int] { + return (0.. Int in + (i < target.count) ? target[i] : 0 + } +} + diff --git a/Sources/OnboardingUI/Processing/Onboarding.swift b/Sources/OnboardingUI/Processing/Onboarding.swift index 27ae862cc..67420414e 100644 --- a/Sources/OnboardingUI/Processing/Onboarding.swift +++ b/Sources/OnboardingUI/Processing/Onboarding.swift @@ -8,15 +8,17 @@ import Foundation import SwiftUI -/// A protocol that allows you to easily configure what is displayed in Onboarding @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +/// A protocol that defines onboarding content for an app. public protocol Onboarding: Identifiable, Sendable { + /// The unique identifier for this onboarding item. var id: UUID { get } /// Onboarding Title var title: Text { get } /// Variables indicating features @FeatureBuilder var features: Array { get } - /// Link to be displayed at the bottom of the screen + + /// Optional link displayed on the onboarding screen. var link: Link? { get } } @@ -25,14 +27,16 @@ public extension Onboarding { var id: UUID { return UUID() } - + // デフォルト実装でnilを返す var link: Link? { return nil } } /// Structures to build features to display onboarding @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +/// A structure representing a single onboarding feature. public struct Feature: Identifiable, Sendable { + /// The unique identifier for this feature. public var id: UUID = UUID() /// Onboarding Item Title public var title: Text = Text(verbatim: "") @@ -48,6 +52,11 @@ public struct Feature: Identifiable, Sendable { self.message = nil } + /// Initializes a feature with title, image and message. + /// - Parameters: + /// - title: The title text of the feature. + /// - image: The image representing the feature. + /// - message: The message text of the feature. public init(title: Text,image: Image?,message: Text?) { self.id = UUID() self.title = title @@ -55,6 +64,11 @@ public struct Feature: Identifiable, Sendable { self.message = message } + /// Initializes a feature using closures to create title, image and message. + /// - Parameters: + /// - title: Closure returning the title text. + /// - image: Closure returning the image. + /// - message: Closure returning the message text. public init(title: () -> Text,image: () -> Image?,message: () -> Text?) { self.id = UUID() self.title = title() @@ -72,32 +86,56 @@ public struct Feature: Identifiable, Sendable { self.image = Image(systemName: imageName) self.message = Text(message) } - /// General initializer + + /// Initializes a feature with localized string resource message. /// - Parameters: /// - title: Title outlining the features /// - imageName: Images showing features - /// - message: Description of features - @_disfavoredOverload public init(_ title: S,imageName: String, message: S) where S : StringProtocol { + /// - message: Description of features as a localized resource + public init(_ title: LocalizedStringKey,imageName: String, messageResource: LocalizedStringResource) { + self.id = UUID() + self.title = Text(title) + self.image = Image(systemName: imageName) + self.message = Text(messageResource) + } + /// Initializes a feature with localized string resource message. + /// - Parameters: + /// - titleResource: Title outlining the features as a localized resource + /// - imageName: Images showing features + /// - messageResource: Description of features as a localized resource + public init(titleResource: LocalizedStringResource,imageName: String, messageResource: LocalizedStringResource) { + self.id = UUID() + self.title = Text(titleResource) + self.image = Image(systemName: imageName) + self.message = Text(messageResource) + } + /// Initializes a feature with string titles and messages. + /// - Parameters: + /// - title: String title of the feature. + /// - imageName: System image name. + /// - message: Description message string. + @_disfavoredOverload + public init(_ title: S,imageName: String, message: S) where S : StringProtocol { self.id = UUID() self.title = Text(title) self.image = Image(systemName: imageName) self.message = Text(message) } - /// Change the title variable from the original Feature structure. - /// - Parameter string: String to be displayed - /// - Returns: Feature structure with modified title variable + /// Returns a new feature with the title replaced. + /// - Parameter string: The new title string. + /// - Returns: A new Feature instance with updated title. public func title(_ string: String) -> Self { .init(title: Text(string), image: self.image, message: self.message) } - /// Change the title variable from the original Feature structure. + /// Returns a new feature with the localized title replaced. /// - Parameters: - /// - key: LocalizedStringKey to be displayed - /// - tableName: The name of the string table to search. If nil, use the table in the Localizable.strings file. - /// - bundle: The bundle containing the strings file. If nil, use the main bundle. - /// - comment: Contextual information about this key-value pair. - /// - Returns: Feature structure with modified title variable + /// - key: The localized string key for the new title. + /// - tableName: The table name for localization. + /// - bundle: The bundle containing the localized strings. + /// - comment: Contextual comment for translators. + /// - Returns: A new Feature instance with updated title. public func title( _ key: LocalizedStringKey, tableName: String? = nil, @@ -107,27 +145,27 @@ public struct Feature: Identifiable, Sendable { .init(title: Text(key, tableName: tableName, bundle: bundle, comment: comment), image: self.image, message: self.message) } - /// Change the image variable from the original Feature structure. - /// - Parameter systemName: The name of the system symbol image. Use the SF Symbols app to look up the names of system symbol images. - /// - Returns: Feature structure with modified image variable + /// Returns a new feature with the image replaced by a system image. + /// - Parameter systemName: The system image name. + /// - Returns: A new Feature instance with updated image. public func image(systemName: String) -> Self { .init(title: self.title, image: Image(systemName: systemName), message: self.message) } - /// Change the message variable from the original Feature structure. - /// - Parameter string: String to be displayed - /// - Returns: Feature structure with modified message variable + /// Returns a new feature with the message replaced. + /// - Parameter string: The new message string. + /// - Returns: A new Feature instance with updated message. public func message(_ string: String) -> Self { .init(title: self.title, image: self.image, message: Text(string)) } - /// Change the message variable from the original Feature structure. + /// Returns a new feature with the localized message replaced. /// - Parameters: - /// - key: LocalizedStringKey to be displayed - /// - tableName: The name of the string table to search. If nil, use the table in the Localizable.strings file. - /// - bundle: The bundle containing the strings file. If nil, use the main bundle. - /// - comment: Contextual information about this key-value pair. - /// - Returns: Feature structure with modified message variable + /// - key: The localized string key for the new message. + /// - tableName: The table name for localization. + /// - bundle: The bundle containing the localized strings. + /// - comment: Contextual comment for translators. + /// - Returns: A new Feature instance with updated message. public func message( _ key: LocalizedStringKey, tableName: String? = nil, @@ -137,3 +175,48 @@ public struct Feature: Identifiable, Sendable { .init(title: self.title, image: self.image, message: Text(key, tableName: tableName, bundle: bundle, comment: comment)) } } +/// Result builder that allows you to freely build Feature structures +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +/// A result builder to construct onboarding features. +@resultBuilder +public struct FeatureBuilder { + /// Required in resultBuilder + /// Combines multiple Features into an array. + /// - Parameter parts: A variadic list of Features. + /// - Returns: An array of Features. + public static func buildBlock(_ parts: Feature...) -> Array { + parts + } + /// Enable if + /// Handles optional Feature arrays. + /// - Parameter parts: An optional array of Features. + /// - Returns: The first Feature or a default Feature. + public static func buildOptional(_ parts: [Feature]?) -> Feature { + parts?.first ?? Feature() + } + /// Enable if-else (first branch) + /// - Parameter parts: An array of Features. + /// - Returns: The first Feature or a default Feature. + public static func buildEither(first parts: [Feature]) -> Feature { + parts.first ?? Feature() + } + /// Enable if-else (second branch) + /// - Parameter parts: An array of Features. + /// - Returns: The first Feature or a default Feature. + public static func buildEither(second parts: [Feature]) -> Feature { + parts.first ?? Feature() + } + /// Enable for-in + /// - Parameter parts: An array of Feature arrays. + /// - Returns: The first Feature from the first array or a default Feature. + public static func buildArray(_ parts: [[Feature]]) -> Feature { + parts.first?.first ?? Feature() + } + /// Enable #if + /// - Parameter parts: An array of Features. + /// - Returns: The array of Features. + public static func buildLimitedAvailability(_ parts: [Feature]) -> Array { + parts + } +} + diff --git a/Sources/OnboardingUI/UI/Environment/EnvironmentValues.swift b/Sources/OnboardingUI/UI/Environment/EnvironmentValues.swift new file mode 100644 index 000000000..5c1c2ec6f --- /dev/null +++ b/Sources/OnboardingUI/UI/Environment/EnvironmentValues.swift @@ -0,0 +1,14 @@ +// +// EnvironmentValues.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/07. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +extension EnvironmentValues { + @Entry internal var onboardingViewStyle: AnyOnboardingViewStyle = AnyOnboardingViewStyle(.automatic) +} diff --git a/Sources/OnboardingUI/UI/Modifier/OnboardingSheet.swift b/Sources/OnboardingUI/UI/Modifier/OnboardingSheet.swift deleted file mode 100644 index 98f337525..000000000 --- a/Sources/OnboardingUI/UI/Modifier/OnboardingSheet.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SheetOnboarding.swift -// -// -// Created by Keisuke Chinone on 2023/12/13. -// - -import SwiftUI - -@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) -struct OnboardingSheet: ViewModifier { - @Binding public var isPresented: Bool - - public let onboarding: any Onboarding - - public func body(content: Content) -> some View { - content - .sheet(isPresented: $isPresented) { - OnboardingSheetView { - onboarding.title - .onboardingTextFormatting(style: .title) - } content: { - ForEach(onboarding.features) { feature in - if let image = feature.image { - OnboardingItem { - image - } content: { - if let message = feature.message { - OnboardingSubtitle(feature.title) - OnboardingContent(message) - } - } - - } - } - } link: { - onboarding.link - } button: { - ContinueButton { - isPresented.toggle() - } - } - } - } -} - -@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) -public extension View { - ///Modifier to display sheets based on Onboarding protocol compliant structures - func onboardingSheet(isPresented: Binding,_ onboarding: Content) -> some View where Content : Onboarding { - modifier(OnboardingUI.OnboardingSheet(isPresented: isPresented, onboarding: onboarding)) - } -} - -#Preview { - @Previewable @State var isPresented: Bool = true - - Group { - Button(String("Open Onboarding")) { - isPresented = true - } -#if os(macOS) - .frame(width: 400, height: 300) -#endif - .onboardingSheet(isPresented: $isPresented, PreviewWhatIsNewOnboarding()) - } -} diff --git a/Sources/OnboardingUI/UI/Modifier/OnboardingTextFormatting.swift b/Sources/OnboardingUI/UI/Modifier/Text+onboardingTextFormatting.swift similarity index 92% rename from Sources/OnboardingUI/UI/Modifier/OnboardingTextFormatting.swift rename to Sources/OnboardingUI/UI/Modifier/Text+onboardingTextFormatting.swift index 0111f4887..2df875340 100644 --- a/Sources/OnboardingUI/UI/Modifier/OnboardingTextFormatting.swift +++ b/Sources/OnboardingUI/UI/Modifier/Text+onboardingTextFormatting.swift @@ -9,6 +9,7 @@ import SwiftUI /// The three items that comprise onboarding @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public enum OnboardingTextFormattingStyle { case title case subtitle @@ -16,6 +17,7 @@ public enum OnboardingTextFormattingStyle { } @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public extension Text { /// Modifier to change the style to suit it. func onboardingTextFormatting(style: OnboardingTextFormattingStyle) -> some View { @@ -42,7 +44,7 @@ public extension Text { #if os(macOS) .font(.body) #elseif os(tvOS) - .font(.subheadline) + .font(.headline) #else .font(.subheadline) #endif @@ -56,8 +58,9 @@ public extension Text { #if os(macOS) .font(.body) #elseif os(tvOS) - .font(.caption) + .font(.body) #else + // .font(.callout) .font(.custom("", size: CGFloat(15.5), relativeTo: .body)) #endif .foregroundColor(.secondary) diff --git a/Sources/OnboardingUI/UI/Modifier/View+onboardingSheet.swift b/Sources/OnboardingUI/UI/Modifier/View+onboardingSheet.swift new file mode 100644 index 000000000..b97a8ccbc --- /dev/null +++ b/Sources/OnboardingUI/UI/Modifier/View+onboardingSheet.swift @@ -0,0 +1,88 @@ +// +// SheetOnboarding.swift +// +// +// Created by Keisuke Chinone on 2023/12/13. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +struct OnboardingSheet: ViewModifier { + @Binding public var isPresented: Bool + + public let onboarding: O + + init(isPresented: Binding, onboarding: O) { + self._isPresented = isPresented + self.onboarding = onboarding + } + + public func body(content: Content) -> some View { + content + .sheet(isPresented: $isPresented) { + if #available(iOS 18.0,macOS 15.0,tvOS 18.0,visionOS 2.0,*) { + OnboardingView(onboarding: onboarding) + } else { + OnboardingSheetView { + onboarding.title + .onboardingTextFormatting(style: .title) + } content: { + ForEach(onboarding.features) { feature in + if let image = feature.image { + OnboardingItem { + image + } content: { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + + } + } + } link: { + onboarding.link + } button: { + ContinueButton { + isPresented.toggle() + } + } + } + } + } +} + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +public extension View { + ///Modifier to display sheets based on Onboarding protocol compliant structures + func onboardingSheet(isPresented: Binding,_ onboarding: Content) -> some View where Content : Onboarding { + modifier( + OnboardingUI + .OnboardingSheet( + isPresented: isPresented, + onboarding: onboarding + ) + ) + } +} + +#Preview { + @Previewable @State var isPresented: Bool = true + + if #available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) { + Button(String("Open Onboarding")) { + isPresented = true + } +#if os(macOS) + .frame(width: 400, height: 300) +#endif + .onboardingSheet(isPresented: $isPresented, PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.glass) + } + +} diff --git a/Sources/OnboardingUI/UI/Modifier/View+onboardingViewStyle.swift b/Sources/OnboardingUI/UI/Modifier/View+onboardingViewStyle.swift new file mode 100644 index 000000000..c5db0637b --- /dev/null +++ b/Sources/OnboardingUI/UI/Modifier/View+onboardingViewStyle.swift @@ -0,0 +1,17 @@ +// +// onboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/07. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +public extension View { + func onboardingViewStyle(_ style: some OnboardingViewStyle) -> some View { + self.environment(\.onboardingViewStyle, AnyOnboardingViewStyle(style)) + } +} + diff --git a/Sources/OnboardingUI/UI/Modifier/ColorButtonStyle.swift b/Sources/OnboardingUI/UI/Style/ButtonStyle/ColorButtonStyle.swift similarity index 95% rename from Sources/OnboardingUI/UI/Modifier/ColorButtonStyle.swift rename to Sources/OnboardingUI/UI/Style/ButtonStyle/ColorButtonStyle.swift index 267107233..7a416b377 100644 --- a/Sources/OnboardingUI/UI/Modifier/ColorButtonStyle.swift +++ b/Sources/OnboardingUI/UI/Style/ButtonStyle/ColorButtonStyle.swift @@ -8,7 +8,8 @@ import SwiftUI /// Modifier to build the look and feel of buttons that continue to be used in onboarding -@available(iOS 17.0,macOS 14.0,visionOS 1.0,*) +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public struct ColorButtonStyle: ButtonStyle { /// Foreground Color var foregroundColor: Color = .white diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle.swift new file mode 100644 index 000000000..141c9eba2 --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle.swift @@ -0,0 +1,188 @@ +// +// OnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/04. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +@preconcurrency public protocol OnboardingViewStyle { + typealias Configuration = OnboardingViewStyleConfiguration + + associatedtype Body: View + + @preconcurrency @ViewBuilder func makeBody(configuration: Configuration) -> Body +} + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +struct AnyOnboardingViewStyle: OnboardingViewStyle { + private var _makeBody: (Configuration) -> AnyView + + init(_ style: S) { + _makeBody = { configuration in + AnyView(style.makeBody(configuration: configuration)) + } + } + + func makeBody(configuration: Configuration) -> some View { + _makeBody(configuration) + } +} + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +public struct OnboardingViewStyleConfiguration: Sendable { + + var dynamicTypeSize: DynamicTypeSize + + var title: Title + + var content: Content + + var footer: Footer + + var dismissButton: DismissButton + + init( + dynamicTypeSize: DynamicTypeSize, + title: Title, + content: Content, + footer: Footer , + dismissButton: DismissButton + ) { + self.dynamicTypeSize = dynamicTypeSize + self.title = title + self.content = content + self.footer = footer + self.dismissButton = dismissButton + } + + struct Title: View, Sendable { + var text: Text + + init(_ title: Text) { + self.text = title + } + + var body: some View { + text + } + } + + @preconcurrency + public struct Content: View, Sendable { + private let items: [Feature] + + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + public init(_ items: [Feature]) { + self.items = items + } + + public var body: some View { + ForEach(items) { feature in + if let image = feature.image { + Group { + if dynamicTypeSize <= .xxxLarge { + HStack(alignment: .top,spacing: 20) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle(Color.accentColor) + .frame(width: 37.5, height: 37.5) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 2.5) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } else { + VStack(alignment: .leading) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle(Color.accentColor) + .frame(width: 37.5, height: 37.5) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 5) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } + } +#if os(tvOS) + .focusable() +#endif + } + } + } + + /// 必ず明示的にクロージャで描画する + public func callAsFunction( + @ViewBuilder _ content: @escaping (Feature) -> Content + ) -> some View { + ForEach(items) { item in + content(item) + } + } + } + + @preconcurrency + public struct Footer: View, Sendable { + var content: AnyView + + init(@ViewBuilder content: () -> V) { + self.content = AnyView(content()) + } + + public var body: some View { + content + } + } + + @preconcurrency public struct DismissButton: View, Sendable { + @Environment(\.dismiss) var dismiss + + var text: Text + + public init(text: Text) { + self.text = text + } + + public var body: some View { + Button { + dismiss() + } label: { + text + } + } + + public func callAsFunction( + @ViewBuilder content: (Text) -> V + ) -> some View { + Button { + dismiss() + } label: { + content(text) + } + } + } +} + diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/AutomaticOnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/AutomaticOnboardingViewStyle.swift new file mode 100644 index 000000000..4b52f7d20 --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/AutomaticOnboardingViewStyle.swift @@ -0,0 +1,35 @@ +// +// AutomaticOnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/17. +// + +import SwiftUI + +@available(iOS 17, macOS 14, tvOS 17, visionOS 1, *) +@available(watchOS, unavailable) +public struct AutomaticOnboardingViewStyle: OnboardingViewStyle { + public func makeBody(configuration: Configuration) -> some View { + if #available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) { + ColoredGlassOnboardingViewStyle().makeBody(configuration: configuration) + } else { + ClassicOnboardingViewStyle().makeBody(configuration: configuration) + } + } +} + +@available(iOS 17, macOS 14, tvOS 17, visionOS 1, *) +@available(watchOS, unavailable) +extension OnboardingViewStyle where Self == AutomaticOnboardingViewStyle { + /// The regular About view style to use with an About view. + public static var automatic: AutomaticOnboardingViewStyle { + return AutomaticOnboardingViewStyle() + } +} + + +#Preview { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.automatic) +} diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/BasicOnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/BasicOnboardingViewStyle.swift new file mode 100644 index 000000000..b8c606f85 --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/BasicOnboardingViewStyle.swift @@ -0,0 +1,135 @@ +// +// BasicOnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/04. +// + +import SwiftUI + +@available(iOS 18, macOS 15, tvOS 18, visionOS 2, *) +@available(watchOS, unavailable) +public struct BasicOnboardingViewStyle: OnboardingViewStyle { + public func makeBody(configuration: Configuration) -> some View { + VStack(alignment: .center) { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 30) { + configuration.title + .font(.title2) + .fontWeight(.bold) + VStack(alignment: .leading, spacing: 20) { + configuration.content { feature in + if let image = feature.image { + Group { + if configuration.dynamicTypeSize <= .xxxLarge { + HStack(alignment: .top,spacing: 20) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 2.5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } else { + VStack(alignment: .leading) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } + } +#if os(tvOS) + .focusable() +#endif + } + } + } + + if configuration.dynamicTypeSize > .xxxLarge { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.borderedProminent) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding([.horizontal, .bottom],40) + .padding(.top, 80) + } + + if configuration.dynamicTypeSize <= .xxxLarge { + VStack(alignment: .center, spacing: 25) { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.borderedProminent) + .padding(.horizontal, 40) + } + } + } + } +} + +@available(iOS 18, macOS 15, tvOS 18, visionOS 2, *) +@available(watchOS, unavailable) +extension OnboardingViewStyle where Self == BasicOnboardingViewStyle { + /// The regular About view style to use with an About view. + public static var basic: BasicOnboardingViewStyle { + return BasicOnboardingViewStyle() + } +} + + +#Preview { + if #available(iOS 18, macOS 15, tvOS 18, visionOS 2, *) { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.basic) + } +} diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ClassicOnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ClassicOnboardingViewStyle.swift new file mode 100644 index 000000000..144fc2bf7 --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ClassicOnboardingViewStyle.swift @@ -0,0 +1,150 @@ +// +// ClassicOnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/04. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +public struct ClassicOnboardingViewStyle: OnboardingViewStyle { + public func makeBody(configuration: Configuration) -> some View { + GeometryReader { geom in + VStack(alignment: .center) { + ScrollView(showsIndicators: false) { + VStack(alignment: .center,spacing: geom.size.height / 60) { + VStack(spacing: 0) { + + configuration.title +#if os(macOS) + .font( + .custom( + "", + size: CGFloat(40), + relativeTo: .largeTitle + ) + ) + .fontWeight(.regular) +#elseif os(visionOS) + .font(.extraLargeTitle2) + .fontWeight(.bold) +#else + .font(.largeTitle) + .fontWeight(.bold) +#endif + .multilineTextAlignment(.center) + .minimumScaleFactor(0.75) + .lineLimit(3) + .accessibilityLabel(configuration.title.text) + .padding(.vertical, geom.size.height / 20) + } + + VStack(alignment: .leading, spacing: 35) { + configuration.content { feature in + if let image = feature.image { + + OnboardingItem { + image + } content: { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + + } + } + } +#if os(iOS) + .frame(maxWidth: 440) +#elseif os(macOS) + .frame(maxWidth: 350) +#elseif os(visionOS) + .frame(maxWidth: 555) +#else + .frame(maxWidth: .infinity) +#endif + if configuration.dynamicTypeSize > .xxxLarge { + configuration.footer + + configuration.dismissButton + .buttonStyle(ColorButtonStyle()) +#if os(iOS) + .padding( + .bottom, + 70 - geom.size.height/15 + geom.size.height/20 + ) +#elseif os(visionOS) + .padding(.bottom, geom.size.height/25) +#elseif os(macOS) + .padding(.bottom, 15 + geom.size.height/20) +#else + .padding(.bottom, geom.size.height/20) +#endif + } + } +#if os(visionOS) + .padding(.top, geom.size.height/25) +#else + .padding(.top, geom.size.height/20) +#endif + } + + if configuration.dynamicTypeSize <= .xxxLarge { + Spacer() + +#if !os(tvOS) + configuration.footer +#if os(macOS) + .padding(30) +#else + .padding(.vertical, 5) +#endif +#endif + + configuration.dismissButton + .buttonStyle(ColorButtonStyle()) +#if os(iOS) + .padding( + .bottom, + 70 - geom.size.height/15 + geom.size.height/20 + ) +#elseif os(visionOS) + .padding(.bottom, geom.size.height/25) +#elseif os(macOS) + .padding(.bottom, 15 + geom.size.height/20) +#else + .padding(.bottom, geom.size.height/20) +#endif + } + } + .frame(maxWidth: .infinity,maxHeight: .infinity) + } +#if os(iOS) + .frame(maxWidth: 500) +#elseif os(macOS) + .frame(minWidth: 600,minHeight: 700, alignment: .center) +#elseif os(visionOS) + .frame(width: 655,height: 695, alignment: .center) +#endif + } +} + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +extension OnboardingViewStyle where Self == ClassicOnboardingViewStyle { + /// The regular About view style to use with an About view. + public static var classic: ClassicOnboardingViewStyle { + return ClassicOnboardingViewStyle() + } +} + + +#Preview { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.classic) +} diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ColoredGlassOnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ColoredGlassOnboardingViewStyle.swift new file mode 100644 index 000000000..4f7b45d4b --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/ColoredGlassOnboardingViewStyle.swift @@ -0,0 +1,135 @@ +// +// GlassOnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/04. +// + +import SwiftUI + +@available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) +@available(watchOS, unavailable) +public struct ColoredGlassOnboardingViewStyle: OnboardingViewStyle { + public func makeBody(configuration: Configuration) -> some View { + VStack(alignment: .center) { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 30) { + configuration.title + .font(.title2) + .fontWeight(.bold) + VStack(alignment: .leading, spacing: 20) { + configuration.content { feature in + if let image = feature.image { + Group { + if configuration.dynamicTypeSize <= .xxxLarge { + HStack(alignment: .top,spacing: 20) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 2.5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } else { + VStack(alignment: .leading) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } + } +#if os(tvOS) + .focusable() +#endif + } + } + } + + if configuration.dynamicTypeSize > .xxxLarge { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.glassProminent) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding([.horizontal, .bottom],40) + .padding(.top, 80) + } + + if configuration.dynamicTypeSize <= .xxxLarge { + VStack(alignment: .center, spacing: 25) { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.glassProminent) + .padding(.horizontal, 40) + } + } + } + } +} + +@available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) +@available(watchOS, unavailable) +extension OnboardingViewStyle where Self == ColoredGlassOnboardingViewStyle { + /// The regular About view style to use with an About view. + public static var coloredGlass: ColoredGlassOnboardingViewStyle { + return ColoredGlassOnboardingViewStyle() + } +} + + +#Preview { + if #available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.coloredGlass) + } +} diff --git a/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/GlassOnboardingViewStyle.swift b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/GlassOnboardingViewStyle.swift new file mode 100644 index 000000000..e31d66e56 --- /dev/null +++ b/Sources/OnboardingUI/UI/Style/OnboardingViewStyle/GlassOnboardingViewStyle.swift @@ -0,0 +1,135 @@ +// +// GlassOnboardingViewStyle.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/17. +// + +import SwiftUI + +@available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) +@available(watchOS, unavailable) +public struct GlassOnboardingViewStyle: OnboardingViewStyle { + public func makeBody(configuration: Configuration) -> some View { + VStack(alignment: .center) { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 30) { + configuration.title + .font(.title2) + .fontWeight(.bold) + VStack(alignment: .leading, spacing: 20) { + configuration.content { feature in + if let image = feature.image { + Group { + if configuration.dynamicTypeSize <= .xxxLarge { + HStack(alignment: .top,spacing: 20) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 2.5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } else { + VStack(alignment: .leading) { + image + .resizable() + .scaledToFit() + .font(.largeTitle) + .foregroundStyle( + Color.accentColor + ) + .frame( + width: 37.5, + height: 37.5 + ) + .accessibilityHidden(true) + + VStack( + alignment: .leading, + spacing: 5 + ) { + if let message = feature.message { + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) + } + } + } + } + } +#if os(tvOS) + .focusable() +#endif + } + } + } + + if configuration.dynamicTypeSize > .xxxLarge { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.glass) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding([.horizontal, .bottom],40) + .padding(.top, 80) + } + + if configuration.dynamicTypeSize <= .xxxLarge { + VStack(alignment: .center, spacing: 25) { + configuration.footer + + configuration.dismissButton { text in + text + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + .buttonStyle(.glass) + .padding(.horizontal, 40) + } + } + } + } +} + +@available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) +@available(watchOS, unavailable) +extension OnboardingViewStyle where Self == GlassOnboardingViewStyle { + /// The regular About view style to use with an About view. + public static var glass: GlassOnboardingViewStyle { + return GlassOnboardingViewStyle() + } +} + + +#Preview { + if #available(iOS 26, macOS 26, tvOS 26, visionOS 26, *) { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + .onboardingViewStyle(.glass) + } +} diff --git a/Sources/OnboardingUI/UI/View/OnboardingCardView.swift b/Sources/OnboardingUI/UI/View/OnboardingCardView.swift index 16a6c9102..749eb7f97 100644 --- a/Sources/OnboardingUI/UI/View/OnboardingCardView.swift +++ b/Sources/OnboardingUI/UI/View/OnboardingCardView.swift @@ -7,8 +7,8 @@ import SwiftUI -/// View to display card-type onboarding -@available(iOS 17.0, macOS 14.0, tvOS 17.0, visionOS 1.0, *) +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public struct OnboardingCardView: View { @Binding var isPresented: Bool var title: V1 @@ -38,14 +38,16 @@ public struct OnboardingCardView: View { image } content: { if let message = feature.message { - OnboardingSubtitle(feature.title) - OnboardingContent(message) + feature.title + .onboardingTextFormatting(style: .subtitle) + message + .onboardingTextFormatting(style: .content) } } } }) } - + /// View public var body: some View { if isPresented { VStack { @@ -56,9 +58,6 @@ public struct OnboardingCardView: View { content } .frame(maxWidth: .infinity) - #if os(tvOS) - .padding(.horizontal, 150) - #endif } .frame(maxWidth: .infinity) .padding(.vertical, 50) @@ -69,8 +68,22 @@ public struct OnboardingCardView: View { isPresented.toggle() }) { Image(systemName: "multiply") + .font(.system(size: 20)) + .frame(width: 30, height: 30) +#if os(visionOS) + .foregroundStyle(.white) + .background(.ultraThinMaterial, in: Circle()) +#else + .foregroundStyle(.gray) + .background(.gray.opacity(0.15), in: Circle()) +#endif +#if !os(macOS) + .hoverEffect() +#endif } - .buttonStyle(DismissButtonStyle()) + .frame(width: 30, height: 30) + .buttonStyle(.borderless) + .padding(10) } } else { EmptyView() diff --git a/Sources/OnboardingUI/UI/View/OnboardingSheetView.swift b/Sources/OnboardingUI/UI/View/OnboardingSheetView.swift index 9c3676247..f8b20398a 100644 --- a/Sources/OnboardingUI/UI/View/OnboardingSheetView.swift +++ b/Sources/OnboardingUI/UI/View/OnboardingSheetView.swift @@ -6,22 +6,26 @@ // import SwiftUI + /// View to show onboarding in sheets @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public struct OnboardingSheetView: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize - /// Title View + /// The view used for the title area. var title: V1 - /// View displaying features + /// The view displaying onboarding features. var content: V2 - /// Link View + + /// The link view shown in the sheet. var link: V3 - /// Button view at the bottom + /// The button view at the bottom of the onboarding sheet. var button: V4 - /// Initializer for the three Views that make up the OnboardingSheet + /// Creates an onboarding sheet view with custom content, title, link, and button views. /// - Parameters: /// - title: Title View /// - content: View displaying features + /// - link: Link view in the sheet /// - button: Button view at the bottom public init( @ViewBuilder title: () -> V1, @@ -34,7 +38,12 @@ public struct OnboardingSheetView: View { self.link = link() self.button = button() } - + /// Creates an onboarding sheet view with a default link view. + /// - Parameters: + /// - title: Title View + /// - content: View displaying features + /// - link: Link view in the sheet (default is EmptyView) + /// - button: Button view at the bottom public init( @ViewBuilder title: () -> V1, @ViewBuilder content: () -> V2, @@ -46,7 +55,7 @@ public struct OnboardingSheetView: View { self.link = link self.button = button() } - /// View + /// The view body providing the onboarding UI layout. public var body: some View { GeometryReader { geom in VStack(alignment: .center) { @@ -55,10 +64,9 @@ public struct OnboardingSheetView: View { VStack(spacing: 0) { title - .padding(.horizontal, 10) .padding(.vertical, geom.size.height / 20) } -#if !os(tvOS) + VStack(alignment: .leading, spacing: 35) { content } @@ -71,35 +79,19 @@ public struct OnboardingSheetView: View { #else .frame(maxWidth: .infinity) #endif -#else - LazyVGrid(columns: Array(repeating: .init(.flexible()), - count: 2), spacing: 100) { - content - } - .padding(.horizontal, 50) -#endif - - VStack { - if dynamicTypeSize > .xxxLarge { - link -#if os(macOS) - .padding(30) -#else - .padding(.vertical, 5) -#endif - - button + if dynamicTypeSize > .xxxLarge { + link + + button #if os(iOS) - .padding(.top, 10) - .padding(.bottom, 70 - geom.size.height/15 + geom.size.height/20) + .padding(.bottom, 70 - geom.size.height/15 + geom.size.height/20) #elseif os(visionOS) - .padding(.bottom, geom.size.height/25) + .padding(.bottom, geom.size.height/25) #elseif os(macOS) - .padding(.bottom, 15 + geom.size.height/20) + .padding(.bottom, 15 + geom.size.height/20) #else - .padding(.bottom, geom.size.height/20) + .padding(.bottom, geom.size.height/20) #endif - } } } #if os(visionOS) @@ -145,31 +137,31 @@ public struct OnboardingSheetView: View { } } -#Preview("Onboarding Sheet 2") { +#Preview("Onboarding Sheet") { OnboardingSheetView { - OnboardingTitle(String("Welcome to\nOnboardingUI")) + Text("Welcome to\nOnboardingUI") + .onboardingTextFormatting(style: .title) } content: { OnboardingItem(systemName: "keyboard",shape: .red) { - OnboardingSubtitle(String("Easy to Make")) - OnboardingContent(String("Onboarding screens like Apple's stock apps can be easily created with SwiftUI.")) + Text("Easy to Make") + .onboardingTextFormatting(style: .subtitle) + Text("Onboarding screens like Apple's stock apps can be easily created with SwiftUI.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "macbook.and.ipad") { - OnboardingSubtitle(String("Not only for iPhone, but also for Mac and iPad")) - OnboardingContent(String("It supports not only iPhone, but also Mac and iPad. Therefore, there is no need to rewrite the code for each device.")) + Text("Not only for iPhone, but also for Mac and iPad") + .onboardingTextFormatting(style: .subtitle) + Text("It supports not only iPhone, but also Mac and iPad. Therefore, there is no need to rewrite the code for each device.") + .onboardingTextFormatting(style: .content) } OnboardingItem(systemName: "macbook.and.iphone",mode: .palette,primary: .primary,secondary: .blue) { - OnboardingSubtitle(String("Customize SF Symbols")) - OnboardingContent(String("It supports multi-colors and hierarchies supported by iOS 15 and macOS 12, so you can customize it as you wish.")) + Text("Customize SF Symbols") + .onboardingTextFormatting(style: .subtitle) + Text("It supports multi-colors and hierarchies supported by iOS 15 and macOS 12, so you can customize it as you wish.") + .onboardingTextFormatting(style: .content) } - -#if os(tvOS) - OnboardingItem(systemName: "ellipsis",shape: .white) { - OnboardingSubtitle(String("Many other benefits")) - OnboardingContent(String("Now, tvOS is also supported, making it easy to create onboarding. Now you can create onboarding for all platforms except watchOS.")) - } -#endif } link: { Link(String("Check our Privacy Policy…"), destination: URL(string: "https://kc-2001ms.github.io/en/privacy.html")!) } button: { diff --git a/Sources/OnboardingUI/UI/View/OnboardingView.swift b/Sources/OnboardingUI/UI/View/OnboardingView.swift new file mode 100644 index 000000000..ed1b9b79b --- /dev/null +++ b/Sources/OnboardingUI/UI/View/OnboardingView.swift @@ -0,0 +1,47 @@ +// +// OnboardingView.swift +// OnboardingUI +// +// Created by 茅根 啓介 on 2025/07/07. +// + +import SwiftUI + +@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) +public struct OnboardingView: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + @Environment( + \.onboardingViewStyle + ) private var onboardingViewStyle: AnyOnboardingViewStyle + + var onboarding: O + + init(onboarding: O) { + self.onboarding = onboarding + } + + public var body: some View { + onboardingViewStyle + .makeBody( + configuration: .init( + dynamicTypeSize: dynamicTypeSize, + title: .init(onboarding.title), + content: .init(onboarding.features), + footer: .init { + if let link = onboarding.link { + link + } + }, + dismissButton: .init(text: Text("Continue", bundle: .module)) + ) + ) + } +} + +#Preview { + if #available(iOS 18, macOS 15, tvOS 18, visionOS 2, *) { + OnboardingView(onboarding: PreviewWhatIsNewOnboarding()) + } +} diff --git a/Sources/OnboardingUI/UI/View/Parts/OnboardingButton.swift b/Sources/OnboardingUI/UI/View/Parts/OnboardingButton.swift index e65413adb..824908ea1 100644 --- a/Sources/OnboardingUI/UI/View/Parts/OnboardingButton.swift +++ b/Sources/OnboardingUI/UI/View/Parts/OnboardingButton.swift @@ -9,6 +9,7 @@ import SwiftUI /// Continue button @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public struct ContinueButton: View { /// Foreground Color let foregroundColor: Color @@ -49,7 +50,7 @@ public struct ContinueButton: View { /// View public var body: some View { Button(action: action) { - Text("Continue", bundle: Bundle.module) + Text("Continue", bundle: .module) } #if !os(tvOS) .buttonStyle(ColorButtonStyle(foregroundColor: foregroundColor, backgroundColor: backgroundColor)) diff --git a/Sources/OnboardingUI/UI/View/Parts/OnboardingContent.swift b/Sources/OnboardingUI/UI/View/Parts/OnboardingContent.swift deleted file mode 100644 index bbd1aef38..000000000 --- a/Sources/OnboardingUI/UI/View/Parts/OnboardingContent.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// OnboardingContent.swift -// -// -// Created by 茅根啓介 on 2023/03/09. -// - -import SwiftUI - -/// Content to be used for onboarding -@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) -public struct OnboardingContent: View { - /// Text - var TextView: Text - /// Initialize with StringProtocol - /// - Parameter text: Display Contents - public init(_ content: S) where S : StringProtocol { - if content is String { - TextView = Text(LocalizedStringKey(content as! String)) - } else { - TextView = Text(content) - } - } - /// Creates a text view that displays localized content identified by a key. - /// - Parameters: - /// - key: The key for a string in the table identified by tableName. - /// - tableName: The name of the string table to search. If nil, use the table in the Localizable.strings file. - /// - bundle: The bundle containing the strings file. If nil, use the main bundle. - /// - comment: Contextual information about this key-value pair. - public init( - _ key: LocalizedStringKey, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ){ - TextView = Text(key,tableName: tableName,bundle: bundle,comment: comment) - } - /// Initialize with String - /// - Parameter text: Display Contents - public init(verbatim content: String) { - TextView = Text(verbatim: content) - } - /// Initialize with AttributedString - /// - Parameter text: Display Contents - public init(_ attributedContent: AttributedString) { - TextView = Text(attributedContent) - } - /// Initialize with LocalizedStringResource - /// - Parameter text: Display Contents - public init(_ resource: LocalizedStringResource) { - TextView = Text(resource) - } - /// Initialize with Text - /// - Parameter text: Display Contents - public init(_ text: Text) { - self.TextView = text - } - /// View - public var body: some View { - TextView - .onboardingTextFormatting(style: .content) - } -} - -#Preview("OnboardingItemContent") { - OnboardingContent(String("Please check the display of this text")) -} diff --git a/Sources/OnboardingUI/UI/View/Parts/OnboardingItem.swift b/Sources/OnboardingUI/UI/View/Parts/OnboardingItem.swift index 603358348..3125e7728 100644 --- a/Sources/OnboardingUI/UI/View/Parts/OnboardingItem.swift +++ b/Sources/OnboardingUI/UI/View/Parts/OnboardingItem.swift @@ -22,6 +22,7 @@ import SwiftUI /// - Content: The type of the content view. /// - S: The type of the shape style. @available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) +@available(watchOS, unavailable) public struct OnboardingItem: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize /// The content view of the onboarding item. diff --git a/Sources/OnboardingUI/UI/View/Parts/OnboardingSubtitle.swift b/Sources/OnboardingUI/UI/View/Parts/OnboardingSubtitle.swift deleted file mode 100644 index 83a8788e6..000000000 --- a/Sources/OnboardingUI/UI/View/Parts/OnboardingSubtitle.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// OnboardingSubtitle.swift -// -// -// Created by 茅根啓介 on 2023/03/09. -// - -import SwiftUI - -/// Subtitle to be used for onboarding -@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) -public struct OnboardingSubtitle: View { - /// Text - var TextView: Text - /// Initialize with StringProtocol - /// - Parameter text: Display Contents - public init(_ content: S) where S : StringProtocol { - if content is String { - TextView = Text(LocalizedStringKey(content as! String)) - } else { - TextView = Text(content) - } - } - /// Creates a text view that displays localized content identified by a key. - /// - Parameters: - /// - key: The key for a string in the table identified by tableName. - /// - tableName: The name of the string table to search. If nil, use the table in the Localizable.strings file. - /// - bundle: The bundle containing the strings file. If nil, use the main bundle. - /// - comment: Contextual information about this key-value pair. - public init( - _ key: LocalizedStringKey, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ){ - TextView = Text(key,tableName: tableName,bundle: bundle,comment: comment) - } - /// Initialize with String - /// - Parameter text: Display Contents - public init(verbatim content: String) { - TextView = Text(verbatim: content) - } - /// Initialize with AttributedString - /// - Parameter text: Display Contents - public init(_ attributedContent: AttributedString) { - TextView = Text(attributedContent) - } - /// Initialize with LocalizedStringResource - /// - Parameter text: Display Contents - public init(_ resource: LocalizedStringResource) { - TextView = Text(resource) - } - /// Initialize with Text - /// - Parameter text: Display Contents - public init(_ text: Text) { - self.TextView = text - } - /// View - public var body: some View { - TextView - .onboardingTextFormatting(style: .subtitle) - } -} - -#Preview("OnboardingItemTitle") { - OnboardingSubtitle(String("Sample Subtitle")) -} diff --git a/Sources/OnboardingUI/UI/View/Parts/OnboardingTitle.swift b/Sources/OnboardingUI/UI/View/Parts/OnboardingTitle.swift deleted file mode 100644 index 6eb37facf..000000000 --- a/Sources/OnboardingUI/UI/View/Parts/OnboardingTitle.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// OnboardingTitle.swift -// -// -// Created by 茅根啓介 on 2023/03/09. -// - -import SwiftUI - -/// Title to be used for onboarding -@available(iOS 17.0,macOS 14.0,tvOS 17.0,visionOS 1.0,*) -public struct OnboardingTitle: View { - /// Text - var TextView: Text - /// Initialize with StringProtocol - /// - Parameter text: Display Contents - public init(_ content: S) where S : StringProtocol { - if content is String { - TextView = Text(LocalizedStringKey(content as! String)) - } else { - TextView = Text(content) - } - } - /// Creates a text view that displays localized content identified by a key. - /// - Parameters: - /// - key: The key for a string in the table identified by tableName. - /// - tableName: The name of the string table to search. If nil, use the table in the Localizable.strings file. - /// - bundle: The bundle containing the strings file. If nil, use the main bundle. - /// - comment: Contextual information about this key-value pair. - public init( - _ key: LocalizedStringKey, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ){ - TextView = Text(key,tableName: tableName,bundle: bundle,comment: comment) - } - /// Initialize with String - /// - Parameter text: Display Contents - public init(verbatim content: String) { - TextView = Text(verbatim: content) - } - /// Initialize with AttributedString - /// - Parameter text: Display Contents - public init(_ attributedContent: AttributedString) { - TextView = Text(attributedContent) - } - /// Initialize with LocalizedStringResource - /// - Parameter text: Display Contents - public init(_ resource: LocalizedStringResource) { - TextView = Text(resource) - } - /// Initialize with Text - /// - Parameter text: Display Contents - public init(_ text: Text) { - self.TextView = text - } - /// View - public var body: some View { - TextView - .onboardingTextFormatting(style: .title) - } -} - - -#Preview("OnboardingTitle") { - OnboardingTitle(String("Sample Title")) -}