diff --git a/FlowStackExample/FlowStackExample.xcodeproj/project.pbxproj b/FlowStackExample/FlowStackExample.xcodeproj/project.pbxproj index 7ea22cf..b916941 100644 --- a/FlowStackExample/FlowStackExample.xcodeproj/project.pbxproj +++ b/FlowStackExample/FlowStackExample.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + ED183AB22BB4D99A0041F7C7 /* FlowStackExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FlowStackExample.entitlements; sourceTree = ""; }; ED8DE15D2A6765F500215165 /* FlowStackExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlowStackExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; ED8DE1642A6765F600215165 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ED8DE1672A6765F600215165 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -64,6 +65,7 @@ ED8DE15F2A6765F500215165 /* FlowStackExample */ = { isa = PBXGroup; children = ( + ED183AB22BB4D99A0041F7C7 /* FlowStackExample.entitlements */, EDA1DD372A67693A00D82A9D /* FlowStackExampleApp.swift */, EDA1DD342A67693A00D82A9D /* ContentView.swift */, EDFD07BB2A6F319E00CB0737 /* ProductRow.swift */, @@ -303,6 +305,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FlowStackExample/FlowStackExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"FlowStackExample/Preview Content\""; @@ -313,6 +316,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -321,6 +325,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.velosmobile.FlowStackExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -332,6 +338,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FlowStackExample/FlowStackExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"FlowStackExample/Preview Content\""; @@ -342,6 +349,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -350,6 +358,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.velosmobile.FlowStackExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/FlowStackExample/FlowStackExample/ContentView.swift b/FlowStackExample/FlowStackExample/ContentView.swift index 7cd1bbd..d3ca278 100644 --- a/FlowStackExample/FlowStackExample/ContentView.swift +++ b/FlowStackExample/FlowStackExample/ContentView.swift @@ -9,18 +9,25 @@ import SwiftUI import FlowStack struct ContentView: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + let cornerRadius: CGFloat = 24 var body: some View { FlowStack { ScrollView { - LazyVStack(alignment: .center, spacing: 24, pinnedViews: [], content: { - ForEach(Product.allProducts) { product in - FlowLink(value: product, configuration: .init(cornerRadius: cornerRadius)) { - ProductRow(product: product, cornerRadius: cornerRadius) - } + Group { + if horizontalSizeClass == .compact { + LazyVStack(alignment: .center, spacing: 24, pinnedViews: [], content: { + content + }) + + } else { + LazyVGrid(columns: [GridItem(.flexible(), spacing: 16), GridItem(.flexible())], alignment: .center, spacing: 16, content: { + content + }) } - }) + } .padding(.horizontal) } .flowDestination(for: Product.self) { product in @@ -28,6 +35,14 @@ struct ContentView: View { } } } + + var content: some View { + ForEach(Product.allProducts) { product in + FlowLink(value: product, configuration: .init(cornerRadius: cornerRadius)) { + ProductRow(product: product, cornerRadius: cornerRadius) + } + } + } } struct ContentView_Previews: PreviewProvider { diff --git a/FlowStackExample/FlowStackExample/FlowStackExample.entitlements b/FlowStackExample/FlowStackExample/FlowStackExample.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/FlowStackExample/FlowStackExample/FlowStackExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/FlowStackExample/FlowStackExample/ProductRow.swift b/FlowStackExample/FlowStackExample/ProductRow.swift index cc0613c..19b05cd 100644 --- a/FlowStackExample/FlowStackExample/ProductRow.swift +++ b/FlowStackExample/FlowStackExample/ProductRow.swift @@ -13,9 +13,12 @@ struct ProductRow: View { var cornerRadius: CGFloat var body: some View { - image(url: product.imageUrl) - .allowsHitTesting(false) // https://stackoverflow.com/a/74711565 + Color.clear .aspectRatio(4 / 3, contentMode: .fill) + .overlay { + image(url: product.imageUrl) + .allowsHitTesting(false) // https://stackoverflow.com/a/74711565 + } .overlay(alignment: .topTrailing) { Text(product.name) .font(.system(size: 48)) diff --git a/Sources/FlowStack/FlowStack.swift b/Sources/FlowStack/FlowStack.swift index b8402a0..1f6455f 100644 --- a/Sources/FlowStack/FlowStack.swift +++ b/Sources/FlowStack/FlowStack.swift @@ -226,6 +226,9 @@ public struct FlowStack: View { .ignoresSafeArea() .zIndex(Double(element.index + 1) - 0.1) .id(element.hashValue) + .onTapGesture { + flowDismissAction() + } } } @@ -233,6 +236,15 @@ public struct FlowStack: View { usesInternalPath ? $internalPath : _path } + private var flowDismissAction: FlowDismissAction { + FlowDismissAction( + onDismiss: { + withTransaction(transaction) { + pathToUse.wrappedValue.removeLast() + } + }) + } + private var transaction: Transaction { var transaction = Transaction(animation: animation) transaction.disablesAnimations = true @@ -265,13 +277,7 @@ public struct FlowStack: View { .environment(\.flowPath, pathToUse) .environment(\.flowTransaction, transaction) .environmentObject(destinationLookup) - .environment(\.flowDismiss, FlowDismissAction( - onDismiss: { - withTransaction(transaction) { - pathToUse.wrappedValue.removeLast() - } - }) - ) + .environment(\.flowDismiss, flowDismissAction) } } diff --git a/Sources/FlowStack/FlowTransition.swift b/Sources/FlowStack/FlowTransition.swift index ed8b2d6..9107aab 100644 --- a/Sources/FlowStack/FlowTransition.swift +++ b/Sources/FlowStack/FlowTransition.swift @@ -99,6 +99,7 @@ extension AnyTransition { @State private var isDisabled: Bool = false @State var isDismissing: Bool = false @State private var snapCornerRadiusZero: Bool = true + @State private var availableSize: CGSize = .zero private var snapshotPercent: CGFloat { max(0, 1 - percent / 0.2) @@ -106,13 +107,22 @@ extension AnyTransition { @Environment(\.flowDismiss) var dismiss @Environment(\.flowTransaction) var transaction + @Environment(\.horizontalSizeClass) var horizontalSizeClass var cornerRadius: CGFloat { context.cornerRadius + ((UIScreen.displayCornerRadius ?? 20) - context.cornerRadius) * percent } + + var isPresentedFullscreen: Bool { + horizontalSizeClass == .compact || availableSize.width - 2 * Constants.minVerticalPadding < Constants.maxWidth + } var conditionalCornerRadius: CGFloat { - if percent >= 1 { - if snapCornerRadiusZero { - return 0 + if isPresentedFullscreen { + if percent >= 1 { + if snapCornerRadiusZero { + return 0 + } else { + return cornerRadius + } } else { return cornerRadius } @@ -143,13 +153,30 @@ extension AnyTransition { let zoomRect = CGRect( x: (proxy.size.width / 2) * percent + rect.midX * (1 - percent) + (pullOffset ?? .zero).x / 3, y: (proxy.size.height / 2) * percent + rect.midY * (1 - percent) + (pullOffset ?? .zero).y / 3, - width: rect.width + ((proxy.size.width - rect.width) * max(0, percent) * (1 - pullPercent)), - height: rect.height + ((proxy.size.height - rect.height) * max(0, percent) * (1 - pullPercent)) + width: rect.width + ((presentationSize(availableSize: proxy.size).width - rect.width) * max(0, percent) * (1 - pullPercent)), + height: rect.height + ((presentationSize(availableSize: proxy.size).height - rect.height) * max(0, percent) * (1 - pullPercent)) ) return zoomRect } + struct Constants { + static let maxWidth: CGFloat = 706 + static let maxHeight: CGFloat = 998 + static let minVerticalPadding: CGFloat = 44 + } + + private func presentationSize(availableSize: CGSize) -> CGSize { + + if horizontalSizeClass == .regular && availableSize.width - 2 * Constants.minVerticalPadding >= Constants.maxWidth { + let width = Constants.maxWidth + let height = min(Constants.maxHeight, availableSize.height - Constants.minVerticalPadding * 2) + return CGSize(width: width, height: height) + } else { + return availableSize + } + } + func body(content: Content) -> some View { GeometryReader { proxy in let zoomRect = zoomRect(with: proxy, anchor: context.overrideAnchor ?? context.anchor, percent: percent, pullOffset: panOffset) @@ -173,6 +200,10 @@ extension AnyTransition { .onPreferenceChange(InteractiveDismissDisabledKey.self) { isDisabled in self.isDisabled = isDisabled } + .preference(key: SizePreferenceKey.self, value: proxy.size) + .onPreferenceChange(SizePreferenceKey.self, perform: { value in + availableSize = value + }) .overlay { if let image = context.snapshot, percent < 1 { Image(uiImage: image) @@ -198,3 +229,11 @@ extension AnyTransition { } } } + +struct SizePreferenceKey: PreferenceKey { + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } + + static var defaultValue: CGSize = .zero +} diff --git a/Sources/FlowStack/UIScreen+DisplayCorners.swift b/Sources/FlowStack/UIScreen+DisplayCorners.swift index 56a6fe1..ea26ce0 100644 --- a/Sources/FlowStack/UIScreen+DisplayCorners.swift +++ b/Sources/FlowStack/UIScreen+DisplayCorners.swift @@ -33,6 +33,6 @@ extension UIScreen { return nil } - return cornerRadius + return cornerRadius > 0 ? cornerRadius : 12 }() }