Skip to content

Commit

Permalink
Merge pull request #90 from skiptools/material3alpha
Browse files Browse the repository at this point in the history
Update material3 to latest alpha. Support interactiveDismissDisabled
  • Loading branch information
aabewhite authored Dec 6, 2024
2 parents 8b92422 + f28d748 commit d5650b8
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 46 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ Support levels:
<summary><code>.fullScreenCover</code></summary>
<ul>
<li><code>func fullScreenCover(isPresented: Binding&lt;Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> any View) -> some View</code></li>
<li>Note that covers are dismissible via swipe and the back button on Android.</li>
<li>See <a href="#modals">Modals</a></li>
</ul>
</details>
</td>
Expand Down Expand Up @@ -1223,6 +1223,17 @@ Support levels:
<td>✅</td>
<td><code>.inset</code></td>
</tr>
<tr>
<td>✅</td>
<td>
<details>
<summary><code>.interactiveDismissDisabled</code></summary>
<ul>
<li>See <a href="#modals">Modals</a></li>
</ul>
</details>
</td>
</tr>
<tr>
<td>✅</td>
<td><code>.italic</code></td>
Expand Down Expand Up @@ -1563,6 +1574,7 @@ Support levels:
<summary><code>.sheet</code> (<a href="https://skip.tools/docs/components/sheet/">example</a>)</summary>
<ul>
<li><code>func sheet(isPresented: Binding&lt;Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> any View) -> some View</code></li>
<li>See <a href="#modals">Modals</a></li>
</ul>
</details>
</td>
Expand Down Expand Up @@ -2315,6 +2327,25 @@ SkipUI supports both of these models. Using `.navigationDestinations`, however,

Compose imposes an additional restriction as well: we must be able to stringify `.navigationDestination` key types. See [Restrictions on Identifiers](#restrictions-on-identifiers) below.

#### Modals

Skip supports standard modal presentations. Android apps typically allow users to dismiss modals with the Android back button. Skip allows you to selectively disable this behavior with the Android-only `backDismissDisabled(_ isDisabled: Bool = true)` SwiftUI modifier. If you use this modifier, you **must** put it on the top-level view embedded in your `.sheet` or `.fullScreenCover`, as in the following example:

```swift
SomeContentView()
.sheet(isPresented: $isSheetPresented) {
SheetContentView()
#if os(Android)
.backDismissDisabled()
#endif
}
```

Due to Compose limitations, changing the value passed to `.backDismissDisabled(_: Bool = true)` while the modal is presented has no effect. Only the value at the time of presentation is considered.
{: class="callout warning"}

Note that you might want to pair `backDismissDisabled` with SwiftUI's `.interactiveDismissDisabled` modifier to also disable dismissing via dragging the sheet down.

### Restrictions on Identifiers

Compose requires all state values to be serializable. This restriction is typically transparent to your code, because when you use property wrappers like `@State`, SkipUI automatically tracks your state objects and gives Compose serializable identifiers in their place. Some SwiftUI values, however, must be stored directly in Compose, including `navigationDestination` values and `List` item identifiers. When this is the case, SkipUI creates a `String` from the value you supply using the following algorithm:
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/Skip/skip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ settings:
- 'library("androidx-compose-animation", "androidx.compose.animation", "animation").withoutVersion()'
- 'library("androidx-compose-material", "androidx.compose.material", "material").withoutVersion()'
- 'library("androidx-compose-material-icons-extended", "androidx.compose.material", "material-icons-extended").withoutVersion()'
- 'library("androidx-compose-material3", "androidx.compose.material3", "material3").withoutVersion()'
- 'library("androidx-compose-material3", "androidx.compose.material3", "material3").version("1.4.0-alpha04")'
- 'library("androidx-compose-foundation", "androidx.compose.foundation", "foundation").withoutVersion()'
- 'library("androidx-appcompat", "androidx.appcompat", "appcompat").versionRef("androidx-appcompat")'
- 'library("androidx-appcompat-resources", "androidx.appcompat", "appcompat-resources").versionRef("androidx-appcompat")'
Expand Down
4 changes: 2 additions & 2 deletions Sources/SkipUI/SkipUI/Animation/Transition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,12 @@ struct TransitionModifierView: ComposeModifierView {

init(view: View, transition: Transition) {
self.transition = transition
super.init(view: view, role: ComposeModifierRole.transition)
super.init(view: view)
}

/// Extract the transition from the given view's modifiers.
static func transition(for view: View) -> Transition? {
guard let modifierView = view.strippingModifiers(until: { $0 == ComposeModifierRole.transition }, perform: { $0 as? TransitionModifierView }) else {
guard let modifierView = view.strippingModifiers(until: { $0 is TransitionModifierView }, perform: { $0 as? TransitionModifierView }) else {
return nil
}
return modifierView.transition
Expand Down
4 changes: 2 additions & 2 deletions Sources/SkipUI/SkipUI/Commands/EditActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ final class EditActionsModifierView: ComposeModifierView {
var isMoveDisabled: Bool?

init(view: View, isDeleteDisabled: Bool? = nil, isMoveDisabled: Bool? = nil) {
super.init(view: view, role: ComposeModifierRole.editActions)
super.init(view: view)
let wrappedEditActionsView = Self.unwrap(view: view)
self.isDeleteDisabled = isDeleteDisabled ?? wrappedEditActionsView?.isDeleteDisabled
self.isMoveDisabled = isMoveDisabled ?? wrappedEditActionsView?.isMoveDisabled
}

/// Return the edit actions modifier information for the given view.
static func unwrap(view: View) -> EditActionsModifierView? {
return view.strippingModifiers(until: { $0 == .editActions }, perform: { $0 as? EditActionsModifierView })
return view.strippingModifiers(until: { $0 is EditActionsModifierView }, perform: { $0 as? EditActionsModifierView })
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Commands/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public final class Menu : View {
}

@Composable private static func ComposeDropdownMenuItem(for view: ComposeBuilder, context: ComposeContext, isSelected: Bool? = nil, action: () -> Void) {
let label = view.collectViews(context: context).first?.strippingModifiers(perform: { $0 as? Label })
let label = view.collectViews(context: context).first?.strippingModifiers { $0 as? Label }
if let isSelected {
let selectedIcon: @Composable () -> Void
if isSelected {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp

// If our content has a zIndex, we need to pull it into our modifiers so that it applies within the original
// parent container. Otherwise the Box we use below would hide it
if let zIndex = view.strippingModifiers(until: { $0 == .zIndex }, perform: { $0 as? ZIndexModifierView }) {
if let zIndex = view.strippingModifiers(until: { $0 is ZIndexModifierView }, perform: { $0 as? ZIndexModifierView }) {
modifier = zIndex.consume(with: modifier)
}

Expand Down
18 changes: 6 additions & 12 deletions Sources/SkipUI/SkipUI/Compose/ComposeModifierView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,35 @@ import androidx.compose.runtime.Composable
/// Recognized modifier roles.
public enum ComposeModifierRole {
case accessibility
case editActions
case gesture
case id
case listItem
case spacing
case tabItem
case tag
case transition
case unspecified
case zIndex
}

/// Used internally by modifiers to apply changes to the context supplied to modified views.
class ComposeModifierView: View {
public class ComposeModifierView: View {
let view: View
let role: ComposeModifierRole
var action: (@Composable (inout ComposeContext) -> ComposeResult)?
var composeContent: (@Composable (any View, ComposeContext) -> Void)?

/// Constructor for subclasses.
init(view: any View, role: ComposeModifierRole = .unspecified) {
public init(view: any View, role: ComposeModifierRole = .unspecified) {
// Don't copy view
// SKIP REPLACE: this.view = view
self.view = view
self.role = role
}

/// A modfiier that performs an action, optionally modifying the `ComposeContext` passed to the modified view.
init(targetView: any View, role: ComposeModifierRole = .unspecified, action: @Composable (inout ComposeContext) -> ComposeResult) {
public init(targetView: any View, role: ComposeModifierRole = .unspecified, action: @Composable (inout ComposeContext) -> ComposeResult) {
self.init(view: targetView, role: role)
self.action = action
}

/// A modifier that takes over the composition of the modified view.
init(contentView: any View, role: ComposeModifierRole = .unspecified, composeContent: @Composable (any View, ComposeContext) -> Void) {
public init(contentView: any View, role: ComposeModifierRole = .unspecified, composeContent: @Composable (any View, ComposeContext) -> Void) {
self.init(view: contentView, role: role)
self.composeContent = composeContent
}
Expand All @@ -61,8 +55,8 @@ class ComposeModifierView: View {
}
}

func strippingModifiers<R>(until: (ComposeModifierRole) -> Bool = { _ in false }, perform: (any View?) -> R) -> R {
if until(role) {
func strippingModifiers<R>(until: (ComposeModifierView) -> Bool = { _ in false }, perform: (any View?) -> R) -> R {
if until(self) {
return perform(self)
} else {
return view.strippingModifiers(until: until, perform: perform)
Expand Down
6 changes: 3 additions & 3 deletions Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ public final class List : View {
return
}

let itemModifierView = view.strippingModifiers(until: { $0 == .listItem }, perform: { $0 as? ListItemModifierView })
let itemModifierView = view.strippingModifiers(until: { $0 is ListItemModifierView }, perform: { $0 as? ListItemModifierView })
var itemModifier: Modifier = Modifier
if itemModifierView?.background == nil {
itemModifier = itemModifier.background(BackgroundColor(styling: styling.withStyle(ListStyle.plain), isItem: isItem))
Expand Down Expand Up @@ -836,10 +836,10 @@ final class ListItemModifierView: ComposeModifierView, ListItemAdapting {
var separator: Visibility?

init(view: View, background: View? = nil, separator: Visibility? = nil) {
let modifierView = view.strippingModifiers(until: { $0 == .listItem }, perform: { $0 as? ListItemModifierView })
let modifierView = view.strippingModifiers(until: { $0 is ListItemModifierView }, perform: { $0 as? ListItemModifierView })
self.background = background ?? modifierView?.background
self.separator = separator ?? modifierView?.separator
super.init(view: view, role: ComposeModifierRole.listItem)
super.init(view: view)
}

@Composable func shouldComposeListItem() -> Bool {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SkipUI/SkipUI/Containers/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public struct TabView : View {
tabViews = content.collectViews(context: tabItemContext).filter { !$0.isSwiftUIEmptyView }
}
let tabItems = tabViews.map { view in
view.strippingModifiers(until: { $0 == .tabItem }, perform: { $0 as? TabItemModifierView })
view.strippingModifiers(until: { $0 is TabItemModifierView }, perform: { $0 as? TabItemModifierView })
}

let navController = rememberNavController()
Expand Down Expand Up @@ -366,7 +366,7 @@ struct TabItemModifierView: ComposeModifierView {

init(view: View, @ViewBuilder label: () -> any View) {
self.label = ComposeBuilder.from(label)
super.init(view: view, role: .tabItem)
super.init(view: view)
}

@Composable public override func ComposeContent(context: ComposeContext) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/VStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ final class VStackComposer: RenderingComposer {
return
}
// If the Text has spacing modifiers, no longer special case its spacing
let isText = view.strippingModifiers(until: { $0 == .spacing }) { $0 is Text }
let isText = view.strippingModifiers(until: { $0.role == .spacing }) { $0 is Text }
var contentContext = context(false)
if let lastViewWasText {
let spacing = lastViewWasText && isText ? Self.textSpacing : Self.defaultSpacing
Expand Down
58 changes: 51 additions & 7 deletions Sources/SkipUI/SkipUI/Layout/Presentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import Foundation
#if SKIP
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
Expand Down Expand Up @@ -35,6 +36,7 @@ import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Surface
import androidx.compose.material3.rememberModalBottomSheetState
Expand Down Expand Up @@ -81,12 +83,15 @@ let overlayPresentationCornerRadius = 16.0
if HandlePresentationSizeClassChange(isPresented: isPresented, isDismissingForSizeClassChange: isDismissingForSizeClassChange) {
return
}


let interactiveDismissDisabledPreference = rememberSaveable(stateSaver: context.stateSaver as! Saver<Preference<Bool>, Any>) { mutableStateOf(Preference<Bool>(key: InteractiveDismissDisabledPreferenceKey.self)) }
let interactiveDismissDisabledCollector = PreferenceCollector<Bool>(key: InteractiveDismissDisabledPreferenceKey.self, state: interactiveDismissDisabledPreference)

let sheetState = rememberModalBottomSheetState(skipPartiallyExpanded: true)
let isPresentedValue = isPresented.get()
let shouldBePresented = !isDismissingForSizeClassChange.value && isPresentedValue
if shouldBePresented || sheetState.isVisible {
let contentView = ComposeBuilder.from(content)
let contentViews = ComposeBuilder.from(content).collectViews(context: context)
let topInset = remember { mutableStateOf(0.dp) }
let topInsetPx = with(LocalDensity.current) { topInset.value.toPx() }
let handleHeight = isFullScreen ? 0.dp : 8.dp
Expand All @@ -97,10 +102,13 @@ let overlayPresentationCornerRadius = 16.0
let y = topInsetPx - handleHeightPx - handlePaddingPx
addRect(Rect(offset = Offset(x: Float(0.0), y: y), size: Size(width: size.width, height: size.height - y)))
}
let interactiveDismissDisabled = isFullScreen || interactiveDismissDisabledPreference.value.reduced
let backDismissDisabled = contentViews.first?.strippingModifiers(until: { $0 is BackDismissDisabledModifierView }) { ($0 as? BackDismissDisabledModifierView)?.isDisabled == true } ?? false
let onDismissRequest = {
isPresented.set(false)
}
ModalBottomSheet(onDismissRequest: onDismissRequest, sheetState: sheetState, containerColor: androidx.compose.ui.graphics.Color.Unspecified, shape: shape, dragHandle: nil, contentWindowInsets: { WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) }) {
let properties = ModalBottomSheetProperties(shouldDismissOnBackPress: !backDismissDisabled)
ModalBottomSheet(onDismissRequest: onDismissRequest, sheetState: sheetState, sheetGesturesEnabled: !interactiveDismissDisabled, containerColor: androidx.compose.ui.graphics.Color.Unspecified, shape: shape, dragHandle: nil, contentWindowInsets: { WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) }, properties: properties) {
let isEdgeToEdge = EnvironmentValues.shared._isEdgeToEdge == true
let sheetDepth = EnvironmentValues.shared._sheetDepth
let verticalSizeClass = EnvironmentValues.shared.verticalSizeClass
Expand All @@ -109,7 +117,7 @@ let overlayPresentationCornerRadius = 16.0
let detentPreferences = rememberSaveable(stateSaver: context.stateSaver as! Saver<Preference<PresentationDetentPreferences>, Any>) { mutableStateOf(Preference<PresentationDetentPreferences>(key: PresentationDetentPreferenceKey.self)) }
let detentPreferencesCollector = PreferenceCollector<PresentationDetentPreferences>(key: PresentationDetentPreferences.self, state: detentPreferences)
let reducedDetentPreferences = detentPreferences.value.reduced

if !isFullScreen && verticalSizeClass != .compact {
systemBarEdges.remove(.top)
if !isEdgeToEdge {
Expand Down Expand Up @@ -164,8 +172,8 @@ let overlayPresentationCornerRadius = 16.0
}
$0.setdismiss(DismissAction(action: { isPresented.set(false) }))
} in: {
PreferenceValues.shared.collectPreferences([detentPreferencesCollector]) {
contentView.Compose(context: context)
PreferenceValues.shared.collectPreferences([interactiveDismissDisabledCollector, detentPreferencesCollector]) {
contentViews.forEach { $0.Compose(context: context) }
}
}
}
Expand Down Expand Up @@ -825,7 +833,7 @@ extension View {
}
)

return sheet(isPresented: isPresented, onDismiss: onDismiss) {
return fullScreenCover(isPresented: isPresented, onDismiss: onDismiss) {
if let unwrappedItem = item.wrappedValue {
content(unwrappedItem)
}
Expand All @@ -845,6 +853,20 @@ extension View {
#endif
}

public func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
#if SKIP
return preference(key: InteractiveDismissDisabledPreferenceKey.self, value: isDisabled)
#else
return self
#endif
}

#if SKIP
public func backDismissDisabled(_ isDisabled: Bool = true) -> some View {
return BackDismissDisabledModifierView(view: self, isDisabled: isDisabled)
}
#endif

public func presentationDetents(_ detents: Set<PresentationDetent>) -> some View {
#if SKIP
// TODO: Add support for multiple detents
Expand Down Expand Up @@ -960,4 +982,26 @@ final class PresentationModifierView: ComposeModifierView {
view.Compose(context: context)
}
}

/// Used disable the back button from dismissing a presentation.
struct BackDismissDisabledModifierView: ComposeModifierView {
let isDisabled: Bool

init(view: View, isDisabled: Bool) {
self.isDisabled = isDisabled
super.init(view: view)
}
}

struct InteractiveDismissDisabledPreferenceKey: PreferenceKey {
typealias Value = Bool

// SKIP DECLARE: companion object: PreferenceKeyCompanion<Boolean>
final class Companion: PreferenceKeyCompanion {
let defaultValue = false
func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue()
}
}
}
#endif
Loading

0 comments on commit d5650b8

Please sign in to comment.