最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - How to resolve SwiftUI animation conflicts - Stack Overflow

programmeradmin3浏览0评论

I built a custom picker that reveals the picker items when tapped. Now I'm trying to use two of those pickers in another view that only shows the picker that the user is currently interacting with and hides the other one but there is a glitch in the picker animation such that hiding the picker pauses for a moment before completely disappearing.

Here is a screen recording of the behaviour -

Here is the code below:

struct ConceptOptionsView: View {
    @State private var aspectRatio: AspectRatio = .sixteenbynine
    @State private var imageModel: ImageModel = .flux(.schnell)
    @State private var resolution: ImageResolution = .seventwenty
    @State private var activePicker: ActivePicker? = nil
    
    enum ActivePicker {
        case model
        case resolution
    }
    
    var body: some View {
        VStack(spacing: 0) {
            Rectangle()
                .fill(.gray.opacity(0.3))
                .frame(height: 0.5)
                .frame(maxWidth: .infinity)
            VStack(alignment: .leading, spacing: 20) {
                
                HStack {
                    if activePicker == nil || activePicker == .model {
                        PillSelectorView(
                            title: "Model",
                            icon: "sparkle",
                            selection: $imageModel,
                            items: ImageModel.pickerItems,
                            didBeginSelection: {
                                activePicker = .model
                            },
                            didEndSelection: {
                                activePicker = nil
                            }
                        )
                        .transition(.move(edge: .leading))
                    }
                    Spacer()
                    if activePicker == nil || activePicker == .resolution {
                        PillSelectorView(
                            title: "Resolution",
                            icon: "camera.metering.center.weighted.average",
                            selection: $resolution,
                            items: ImageResolution.pickerItems,
                            didBeginSelection: { activePicker = .resolution },
                            didEndSelection: { activePicker = nil }
                        )
                        .transition(.move(edge: .trailing))
                    }
                }
                
                Text("The options you select will be the starting point when you want to generate a new image")
                    .font(.footnote)
                    .foregroundStyle(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 20)
            }
            .padding()
            Spacer()
        }
        .background(.ultraThinMaterial)
        .animation(.snappy, value: activePicker)
    }
}
struct PillSelectorItem<SelectionValue: Hashable>: Identifiable {
    let id = UUID()
    let value: SelectionValue
    let title: String
    
    init(
        value: SelectionValue,
        title: String
    ) {
        self.value = value
        self.title = title
    }
}

struct PillSelectorView<SelectionValue: Hashable>: View {
    let title: String
    let icon: String
    @Binding var selection: SelectionValue
    let items: [PillSelectorItem<SelectionValue>]
    var didBeginSelection: () -> Void
    var didEndSelection: () -> Void
    
    @State private var isSelecting: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            Text(title)
                .font(.caption.weight(.medium))
                .foregroundStyle(.secondary)
                .padding(.leading, 10)
            HStack {
                Button {
                    withAnimation {
                        isSelecting.toggle()
                    }
                } label: {
                    HStack {
                        Image(systemName: icon)
                            .imageScale(.small)
                        if !isSelecting {
                            Text(items.first(where: { $0.value == selection })?.title ?? "Item")
                                .font(.subheadline.weight(.medium))
                        }
                    }
                    .padding(.horizontal)
                    .padding(.vertical, 12)
                    .frame(maxWidth: isSelecting ? 40 : .infinity)
                    .background(.gray.opacity(0.1), in: .rect(cornerRadius: 20))
                }
                .buttonStyle(.plain)

                if isSelecting {
                    ScrollView(.horizontal) {
                        HStack {
                            ForEach(items) { item in
                                Button {
                                    withAnimation {
                                        selection = item.value
                                        isSelecting = false
                                    }
                                } label: {
                                    Text(item.title)
                                        .font(.footnote.weight(.medium))
                                        .foregroundStyle(
                                            item.value == selection ? .primary : .secondary
                                        )
                                        .padding(.vertical, 10)
                                        .padding(.horizontal, 10)
                                        .overlay {
                                            Capsule()
                                                .fill(
                                                    item.value == selection ? .gray.opacity(0.1) : .clear
                                                )
                                                .stroke(
                                                    item.value == selection
                                                        ? .primary : Color.gray.opacity(0.3), lineWidth: 1.0
                                                )
                                        }
                                }
                                .buttonStyle(.plain)
                            }
                        }
                    }
                    .scrollIndicators(.hidden)
                    .transition(.move(edge: .trailing))
                }
            }
        }
        .onChange(of: isSelecting) { oldValue, newValue in
            newValue ? didBeginSelection() : didEndSelection()
        }
    }
}

I built a custom picker that reveals the picker items when tapped. Now I'm trying to use two of those pickers in another view that only shows the picker that the user is currently interacting with and hides the other one but there is a glitch in the picker animation such that hiding the picker pauses for a moment before completely disappearing.

Here is a screen recording of the behaviour - https://imgur/a/gItlRIt

Here is the code below:

struct ConceptOptionsView: View {
    @State private var aspectRatio: AspectRatio = .sixteenbynine
    @State private var imageModel: ImageModel = .flux(.schnell)
    @State private var resolution: ImageResolution = .seventwenty
    @State private var activePicker: ActivePicker? = nil
    
    enum ActivePicker {
        case model
        case resolution
    }
    
    var body: some View {
        VStack(spacing: 0) {
            Rectangle()
                .fill(.gray.opacity(0.3))
                .frame(height: 0.5)
                .frame(maxWidth: .infinity)
            VStack(alignment: .leading, spacing: 20) {
                
                HStack {
                    if activePicker == nil || activePicker == .model {
                        PillSelectorView(
                            title: "Model",
                            icon: "sparkle",
                            selection: $imageModel,
                            items: ImageModel.pickerItems,
                            didBeginSelection: {
                                activePicker = .model
                            },
                            didEndSelection: {
                                activePicker = nil
                            }
                        )
                        .transition(.move(edge: .leading))
                    }
                    Spacer()
                    if activePicker == nil || activePicker == .resolution {
                        PillSelectorView(
                            title: "Resolution",
                            icon: "camera.metering.center.weighted.average",
                            selection: $resolution,
                            items: ImageResolution.pickerItems,
                            didBeginSelection: { activePicker = .resolution },
                            didEndSelection: { activePicker = nil }
                        )
                        .transition(.move(edge: .trailing))
                    }
                }
                
                Text("The options you select will be the starting point when you want to generate a new image")
                    .font(.footnote)
                    .foregroundStyle(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 20)
            }
            .padding()
            Spacer()
        }
        .background(.ultraThinMaterial)
        .animation(.snappy, value: activePicker)
    }
}
struct PillSelectorItem<SelectionValue: Hashable>: Identifiable {
    let id = UUID()
    let value: SelectionValue
    let title: String
    
    init(
        value: SelectionValue,
        title: String
    ) {
        self.value = value
        self.title = title
    }
}

struct PillSelectorView<SelectionValue: Hashable>: View {
    let title: String
    let icon: String
    @Binding var selection: SelectionValue
    let items: [PillSelectorItem<SelectionValue>]
    var didBeginSelection: () -> Void
    var didEndSelection: () -> Void
    
    @State private var isSelecting: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            Text(title)
                .font(.caption.weight(.medium))
                .foregroundStyle(.secondary)
                .padding(.leading, 10)
            HStack {
                Button {
                    withAnimation {
                        isSelecting.toggle()
                    }
                } label: {
                    HStack {
                        Image(systemName: icon)
                            .imageScale(.small)
                        if !isSelecting {
                            Text(items.first(where: { $0.value == selection })?.title ?? "Item")
                                .font(.subheadline.weight(.medium))
                        }
                    }
                    .padding(.horizontal)
                    .padding(.vertical, 12)
                    .frame(maxWidth: isSelecting ? 40 : .infinity)
                    .background(.gray.opacity(0.1), in: .rect(cornerRadius: 20))
                }
                .buttonStyle(.plain)

                if isSelecting {
                    ScrollView(.horizontal) {
                        HStack {
                            ForEach(items) { item in
                                Button {
                                    withAnimation {
                                        selection = item.value
                                        isSelecting = false
                                    }
                                } label: {
                                    Text(item.title)
                                        .font(.footnote.weight(.medium))
                                        .foregroundStyle(
                                            item.value == selection ? .primary : .secondary
                                        )
                                        .padding(.vertical, 10)
                                        .padding(.horizontal, 10)
                                        .overlay {
                                            Capsule()
                                                .fill(
                                                    item.value == selection ? .gray.opacity(0.1) : .clear
                                                )
                                                .stroke(
                                                    item.value == selection
                                                        ? .primary : Color.gray.opacity(0.3), lineWidth: 1.0
                                                )
                                        }
                                }
                                .buttonStyle(.plain)
                            }
                        }
                    }
                    .scrollIndicators(.hidden)
                    .transition(.move(edge: .trailing))
                }
            }
        }
        .onChange(of: isSelecting) { oldValue, newValue in
            newValue ? didBeginSelection() : didEndSelection()
        }
    }
}
Share Improve this question edited Mar 7 at 9:16 Benzy Neez 23.8k3 gold badges15 silver badges45 bronze badges asked Mar 7 at 6:49 Oluwatobi OmotayoOluwatobi Omotayo 1,88116 silver badges30 bronze badges 2
  • Please add the definitions of AspectRatio, ImageModel and ImageResolution. Also, which iOS version are you targeting? – Benzy Neez Commented Mar 7 at 8:09
  • @BenzyNeez I'm targeting iOS 18+ Will add the definitions but there are just plain enum cases. – Oluwatobi Omotayo Commented Mar 7 at 10:59
Add a comment  | 

1 Answer 1

Reset to default 1

Inside ConceptOptionsView, you are using a .move transition for the PillSelectorView as they are revealed and hidden. But inside PillSelectorView there is another .move transition on the ScrollView. So there is some duplication here.

The animation can be improved with the following changes:

  1. In ConceptOptionsView, change both the transitions to .opacity. For example:
PillSelectorView(
    // ...
)
.transition(.opacity)
// .transition(.move(edge: .leading))
  1. In PillSelectorView, combine the .move transition with .opacity:
ScrollView(.horizontal) {
    // ...
}
.scrollIndicators(.hidden)
.transition(.move(edge: .trailing)bined(with: .opacity))
  1. Also in PillSelectorView, add the modifier .drawingGroup() to the label of the Button. This keeps the text together with the shape while the animation is happening:
Text(item.title)
    // + all existing modifiers
    .drawingGroup()

Alternatively, if your target is iOS 17 or above, you can add the modifier .geometryGroup() to the ScrollView instead. This actually works a bit better.

Here is how it looks with these changes:

Btw, it seems a bit strange to be showing the capsule shape as an overlay with a semi-transparent fill. This may be why the pill labels have a fuzzy border in the middle. You might want to consider showing the shape in the background of the label instead. Better still, use a custom ButtonStyle.

Also, you will notice that the text below the picker is moving up when isSelecting is true. The reason is because a different font size is being used for this mode (.footnote instead of .subheadline) and the vertical padding is also different (10 instead of 12).

发布评论

评论列表(0)

  1. 暂无评论