Broken SwiftUI Transitions - swift

I have a pretty simple view modifier for presenting toasts
struct ToastItemModifier<Item: Equatable, M: View>: ViewModifier {
typealias Style = ToastStyle
// MARK: - Environment
#Environment(\.colorScheme)
var colorScheme
// MARK: - Properties
private let style: Style
private let item: Item?
private let message: (Item) -> M
#Binding
private var isPresented: Bool
init(item: Item?, isPresented: Binding<Bool>, #ViewBuilder message: #escaping (Item) -> M, style: Style) {
self.style = style
self.item = item
self.message = message
_isPresented = isPresented
}
/// Indicates if content is presented.
private var contentIsPresented: Bool {
item != nil && isPresented
}
private var animation: Animation {
.easeInOut(duration: 1)
}
// MARK: - ViewModifier
func body(content: Content) -> some View {
ZStack {
content
ZStack {
if let item = item, contentIsPresented {
overlay(item: item)
.transition(.asymmetric(insertion: .scale(scale: 0.5), removal: .opacity).animation(animation))
}
}
}
.onReceive(Timer.publish(every: 3, on: .main, in: .default).autoconnect().first()) { _ in
isPresented = false
}
}
private func overlay(item: Item) -> some View {
HStack {
Image(uiImage: style.image)
.padding(.leading, 20)
message(item)
.font(Font.custom("DMSans-Bold", size: 18))
.foregroundColor(.white)
.padding(.trailing)
}
.frame(height: 66)
.background(style.background(for: colorScheme))
.cornerRadius(20)
.edgesIgnoringSafeArea(.all)
.shadow(color: Color.black.opacity(0.15), radius: 12, x: 0, y: 8)
}
}
Using an asymmetric transition, I can achieve a scale upon insertion and an opacity fade upon removal. However, changing those transitions to ones like .slide or .move, completely breaks the transition.
Are certain transitions broken in SwiftUI? I've had great success with opacity and scale but the other ones don't seem to work.

See my comment to question (above) and I'd move animation to container, like
ZStack {
if let item = item, contentIsPresented {
overlay(item: item)
.transition(.asymmetric(insertion: .scale(scale: 0.5), removal: .opacity))
}
}
.animation(animation, value: isPresented) // << here !!

Related

SwiftUI: Publishing changes from async model

I'm attempting to figure out how to display a message to my users when some asynchronous code takes some time to run. So far I've used a sample I found online to create a popup banner and tied the message together using an ObservedObject of the async method on my view and then Publish the values from my async method.
My sample code project is on a public GitHub repository here and I'll post the code at the bottom.
Right now I have an issue when setting the variables from the async method: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates. Solutions online seem to fix this issue by updating the value on the #mainActor thread but I want these methods to run asynchronously AND update the user on what's happening. What's the best way to update my variables from this location?
CODE
in the main app:
var body: some Scene {
WindowGroup {
ContentView(asyncmethod: myAsyncViewModel())
}
}
ContentView:
import SwiftUI
struct ContentView: View {
#State private var isLoaderPresented = false
#State private var isTopMessagePresented = false
#ObservedObject var asyncmethod: myAsyncViewModel
var body: some View {
VStack {
Spacer()
Button( action: {
Task {
isTopMessagePresented = true
let response = await asyncmethod.thisMethodTakesTime()
// Want to return a string or object so I know what happens.
print("Response Loader: \(response ?? "no response")")
isTopMessagePresented = false
}
},
label: { Text("Run Top Banner Code") }
)
Spacer()
}
.foregroundColor(.black)
.popup(isPresented: isTopMessagePresented, alignment: .top, direction: .top, content: {
Snackbar(showForm: $isTopMessagePresented, asyncmethod: asyncmethod)
})
}
}
struct Snackbar: View {
#Binding var showForm: Bool
#ObservedObject var asyncmethod: myAsyncViewModel
var body: some View {
VStack {
HStack() {
Image(systemName: asyncmethod.imageName)
.resizable()
.aspectRatio(contentMode: ContentMode.fill)
.frame(width: 40, height: 40)
Spacer()
VStack(alignment: .center, spacing: 4) {
Text(asyncmethod.title)
.foregroundColor(.black)
.font(.headline)
Text(asyncmethod.subTitle)
.font(.body)
.foregroundColor(.black)
.frame(maxWidth: .infinity)
}
}
.frame(minWidth: 200)
}
.padding(15)
.frame(maxWidth: .infinity, idealHeight: 100)
.background(Color.black.opacity(0.1))
}
}
My async sample method:
import Foundation
class myAsyncViewModel: ObservableObject {
#Published var imageName: String = "questionmark"
#Published var title: String = "title"
#Published var subTitle: String = "subtitle"
func thisMethodTakesTime() async -> String? {
print("In method: \(imageName), \(title), \(subTitle)")
title = "MY METHOD"
subTitle = "Starting out!"
print("In method. Starting \(title)")
subTitle = "This is the message"
print("Sleeping")
try? await Task.sleep(nanoseconds: 1_000_000_000)
subTitle = "Between"
try? await Task.sleep(nanoseconds: 1_000_000_000)
print("After sleep. Ending")
subTitle = "About to return. Success!"
print("In method: \(imageName), \(title), \(subTitle)")
return "RETURN RESULT"
}
}
And the supporting file for the popup:
import SwiftUI
struct Popup<T: View>: ViewModifier {
let popup: T
let isPresented: Bool
let alignment: Alignment
let direction: Direction
// 1.
init(isPresented: Bool, alignment: Alignment, direction: Direction, #ViewBuilder content: () -> T) {
self.isPresented = isPresented
self.alignment = alignment
self.direction = direction
popup = content()
}
// 2.
func body(content: Content) -> some View {
content
.overlay(popupContent())
}
// 3.
#ViewBuilder private func popupContent() -> some View {
GeometryReader { geometry in
if isPresented {
withAnimation {
popup
.transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
.frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
}
}
}
}
}
extension Popup {
enum Direction {
case top, bottom
func offset(popupFrame: CGRect) -> CGFloat {
switch self {
case .top:
let aboveScreenEdge = -popupFrame.maxY
return aboveScreenEdge
case .bottom:
let belowScreenEdge = UIScreen.main.bounds.height - popupFrame.minY
return belowScreenEdge
}
}
}
}
private extension GeometryProxy {
var belowScreenEdge: CGFloat {
UIScreen.main.bounds.height - frame(in: .global).minY
}
}
extension View {
func popup<T: View>(
isPresented: Bool,
alignment: Alignment = .center,
direction: Popup<T>.Direction = .bottom,
#ViewBuilder content: () -> T
) -> some View {
return modifier(Popup(isPresented: isPresented, alignment: alignment, direction: direction, content: content))
}
}
Again all this can be found in my GitHub page here.
You can annotate the observable class or just the function with ‘#MainActor’ or use DispatchQueue.main.async when you assign to the published variables.

How can I detect press gesture within ButtonStyle in SwiftUI? [duplicate]

I have a Button. I want to set custom background color for highlighted state. How can I do it in SwiftUI?
Button(action: signIn) {
Text("Sign In")
}
.padding(.all)
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
.font(Font.body.bold())
Updated for SwiftUI beta 5
SwiftUI does actually expose an API for this: ButtonStyle.
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.blue)
.cornerRadius(8.0)
}
}
// To use it
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle())
As far as I can tell, theres no officially supported way to do this as of yet. Here is a little workaround that you can use. This produces the same behavior as in UIKit where tapping a button and dragging your finger off of it will keep the button highlighted.
struct HoverButton<Label: View>: View {
private let action: () -> ()
private let label: () -> Label
init(action: #escaping () -> (), label: #escaping () -> Label) {
self.action = action
self.label = label
}
#State private var pressed: Bool = false
var body: some View {
Button(action: action) {
label()
.foregroundColor(pressed ? .red : .blue)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in self.pressed = true }
.onEnded { _ in self.pressed = false })
}
}
}
I was looking for a similar functionality and I did it in the following way.
I created a special View struct returning a Button in the style I need, in this struct I added a State property selected. I have a variable named 'table' which is an Int since my buttons a round buttons with numbers on it
struct TableButton: View {
#State private var selected = false
var table: Int
var body: some View {
Button("\(table)") {
self.selected.toggle()
}
.frame(width: 50, height: 50)
.background(selected ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
Then I use in my content View the code
HStack(spacing: 10) {
ForEach((1...6), id: \.self) { table in
TableButton(table: table)
}
}
This creates an horizontal stack with 6 buttons which color blue when selected and red when deselected.
I am not a experienced developer but just tried all possible ways until I found that this is working for me, hopefully it is useful for others as well.
This is for the people who are not satisfied with the above solutions, as they raise other problems such as overlapping gestures(for example, it's quite hard to use this solution in scrollview now). Another crutch is to create a custom button style like this
struct CustomButtonStyle<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Self.Configuration) -> some View {
return change(configuration.isPressed)
}
}
So, we should just transfer the closure which will return the state of the button and create the button based on this parameter. It will be used like this:
struct CustomButton<Content>: View where Content: View {
var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
Button(action: { }, label: {
EmptyView()
})
.buttonStyle(CustomButtonStyle(change: { bool in
Text("\(bool ? "yo" : "yo2")")
}))
}
}
Okey let me clear everything again. Here is the exact solution
Create the below button modifier.
struct StateableButton<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Configuration) -> some View {
return change(configuration.isPressed)
}
}
Then use it like below one
Button(action: {
print("Do something")
}, label: {
// Don't create your button view in here
EmptyView()
})
.buttonStyle(StateableButton(change: { state in
// Create your button view in here
return HStack {
Image(systemName: "clock.arrow.circlepath")
Text(item)
Spacer()
Image(systemName: "arrow.up.backward")
}
.padding(.horizontal)
.frame(height: 50)
.background(state ? Color.black : Color.clear)
}))
You need to define a custom style that can be used to provide the two backgrounds for normal and highlighted states:
Button(action: {
print("action")
}, label: {
Text("My Button").padding()
})
.buttonStyle(HighlightableButtonStyle(normal: { Color.red },
highlighted: { Color.green }))
// Custom button style
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct HighlightableButtonStyle<N, H>: ButtonStyle where N: View, H: View {
private let alignment: Alignment
private let normal: () -> N
private let highlighted: () -> H
init(alignment: Alignment = .center, #ViewBuilder normal: #escaping () -> N, #ViewBuilder highlighted: #escaping () -> H) {
self.alignment = alignment
self.normal = normal
self.highlighted = highlighted
}
func makeBody(configuration: Configuration) -> some View {
return ZStack {
if configuration.isPressed {
configuration.label.background(alignment: alignment, content: normal)
}
else {
configuration.label.background(alignment: alignment, content: highlighted)
}
}
}
}

Disable animation when a view appears in SwiftUI

I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result

Locating position of another View in SwiftUI

I am building a card game (Set) and when dealing the cards I am currently using
withAnimation{} and .transition(.move(edge: .bottom)
to have the cards animate from the bottom edge of the screen when the user taps the deal button. I want to make it so that the cards 'fly out' of the button and was wondering how I find the location of the button.
Once I find the position of the button I intend to use .offset to have them 'fly' out. Is there a better way?
I know it is possible using geometry reader, but I have not found out how to do so.
Here is my current code
struct SetGameView: View {
#ObservedObject var viewModel: SetViewModel = SetViewModel()
var location: (CGFloat, CGFloat) = (0.0, 0.0)
var body: some View {
VStack {
HStack {
Label("Score: \(viewModel.score)", systemImage: "face.dashed").font(.title).padding(.leading)
Spacer()
}
Grid(viewModel.dealtCards) { card in
CardView(card: card)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .leading)))
.padding()
.onTapGesture { withAnimation { viewModel.choose(card) } }
} .onAppear { withAnimation(.easeInOut) { deal(12) } }
Divider()
HStack {
CreateNewGameButton(viewModel: viewModel)
DealNewCardbutton(viewModel: viewModel, deal: deal)
}.padding(.horizontal).frame(maxHeight: 50)
}
}
struct DealNewCardbutton: View {
var viewModel: SetViewModel
let deal: (Int) -> Void
init(viewModel: SetViewModel, deal: #escaping (Int) -> Void) {
self.viewModel = viewModel
self.deal = deal
}
var body: some View {
GeometryReader { geo in
Button(action: {
deal(3)
}){
ZStack {
RoundedRectangle(cornerRadius: 10.0).foregroundColor(.blue)
Text("Deal Three Cards").foregroundColor(.white)
}
}.onAppear {
print(geo.frame(in: .global).midX, geo.frame(in: .global).midY)
}
}
}
}
Heres a video of how it currently works.
https://i.imgur.com/NJfOjBP.mp4
I want the cards to all 'fly out' from the deal button.
Ok. This is pretty tricky. You can get the position of the button via GeometryGetter, but I'd recommend using anchorPreference. The only downside is that you'll have to specify your grid in an overlay or background of the view containing the button.
import SwiftUI
struct BoundsPreferenceKey: PreferenceKey {
static var defaultValue: [Int: Anchor<CGRect>] = [:]
static func reduce(value: inout [Int: Anchor<CGRect>], nextValue: () -> [Int: Anchor<CGRect>]) {
value.merge(nextValue()) { v1, _ in v1 }
}
}
extension View {
func reportBounds(id: Int) -> some View {
self.anchorPreference(key: BoundsPreferenceKey.self, value: .bounds, transform: { [id: $0] })
}
}
private extension Int {
static let dealButtonId = 0
}
var body: some View {
VStack {
Divider()
HStack {
CreateNewGameButton(viewModel: viewModel)
DealNewCardbutton(viewModel: viewModel, deal: deal)
.reportBounds(id: .dealButtonId)
} .padding(.horizontal).frame(maxHeight: 50)
} .overlayPreferenceValue(BoundsPreferenceKey.self) { preferences in
GeometryReader { geometry in
self.getOverlay(preferences: preferences, geometry: geometry)
}
}
}
func getOverlay(preferences:(preferences : [Int: Anchor<CGRect>], geometry: GeometryProxy) -> some View {
var center: CGPoint = .zero
if let anchor = preferences[.dealButtonId] {
let rect = geometry[anchor]
center = CGPoint(x: rect.midX, y: rect.midY)
}
return VStack {
HStack {
Label("Score: \(viewModel.score)", systemImage: "face.dashed").font(.title).padding(.leading)
Spacer()
}
Grid(viewModel.dealtCards) { card in
CardView(card: card)
// USE CENTER here in your transition and/or offset
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .leading)))
.padding()
.onTapGesture { withAnimation { viewModel.choose(card) } }
} .onAppear { withAnimation(.easeInOut) { deal(12) } }
Spacer()
}
}
Note that I moved the grid and other views to the overlay, you may need to set explicit frame heights (to the spacer) to make sure the bottom of the grid and the top of the button align. I left the actual implementation of the transition for you.

SwiftUI ObservedObject in View has two references (instances) BUG

I do not why but I have very frustrating bug in my SwiftUI view.
This view has reference to ViewModel object. But this View is created multiple times on screen appear, and at the end the single View have multiple references to ViewModel object.
I reference this view model object in custom Binding setter/getter or in closure. But object references in Binding and in closure are totally different. This causes many problems with proper View refreshing or saving changes.
struct DealDetailsStagePicker : View {
// MARK: - Observed
#ObservedObject var viewModel: DealDetailsStageViewModel
// MARK: - State
/// TODO: It is workaround as viewModel.dealStageId doesn't work correctly
/// viewModel object is instantiated several times and pickerBinding and onDone
/// closure has different references to viewModel object
/// so updating dealStageId via pickerBinding refreshes it in different viewModel
/// instance than onDone closure executed changeDealStage() method (where dealStageId
/// property stays with initial or nil value.
#State var dealStageId: String? = nil
// MARK: - Binding
#Binding private var showPicker: Bool
// MARK: - Properties
let deal : Deal
// MARK: - Init
init(deal: Deal, showPicker: Binding<Bool>) {
self.deal = deal
self._showPicker = showPicker
self.viewModel = DealDetailsStageViewModel(dealId: deal.id!)
}
var body: some View {
let pickerBinding = Binding<String>(get: {
if self.viewModel.dealStageId == nil {
self.viewModel.dealStageId = self.dealStage?.id ?? ""
}
return self.viewModel.dealStageId!
}, set: { id in
self.viewModel.dealStageId = id //THIS viewModel is reference to object 0x8784783
self.dealStageId = id
})
return VStack(alignment: .leading, spacing: 4) {
Text("Stage".uppercased())
Button(action: {
self.showPicker = true
}) {
HStack {
Text("\(deal.status ?? "")")
Image(systemName: "chevron.down")
}
.contentShape(Rectangle())
}
}
.buttonStyle(BorderlessButtonStyle())
.adaptivePicker(isPresented: $showPicker, selection: pickerBinding, popoverSize: CGSize(width: 400, height: 200), popoverArrowDirection: .up, onDone: {
// save change
self.viewModel.changeDealStage(self.dealStages, self.dealStageId) // THIS viewModel references 0x92392983
}) {
ForEach(self.dealStages, id: \.id) { stage in
Text(stage.name)
.foregroundColor(Color("Black"))
}
}
}
}
I am experiencing this problem in multiple places writing SwiftUI code.
I have several workarounds:
1) as you can see here I use additional #State var to store dealStageId and pass it to viewModale.changeDealStage() instead of updating it on viewModal
2) in other places I am using Wrapper View around such view, then add #State var viewModel: SomeViewModel, then pass this viewModel and assign to #ObservedObject.
But this errors happens randomly depending on placement this View as Subview of other Views. Sometimes it works, sometime it does not work.
It is very od that SINGLE view can have references to multiple view models even if it is instantiated multiple times.
Maybe the problem is with closure as it keeps reference to first ViewModel instance and then this closure is not refreshed in adaptivePicker view modifier?
Workarounds around this issue needs many debugging and boilerplate code to write!
Anyone can help what I am doing wrong or what is wrong with SwiftUI/ObservableObject?
UPDATE
Here is the usage of this View:
private func makeDealHeader() -> some View {
VStack(spacing: 10) {
Spacer()
VStack(spacing: 4) {
Text(self.deal?.name ?? "")
Text(NumberFormatter.price.string(from: NSNumber(value: Double(self.deal?.amount ?? 0)/100.0))!)
}.frame(width: UIScreen.main.bounds.width*0.667)
HStack {
if deal != nil {
DealDetailsStagePicker(deal: self.deal!, showPicker: self.$showStagePicker)
}
Spacer(minLength: 24)
if deal != nil {
DealDetailsClientPicker(deal: self.deal!, showPicker: self.$showClientPicker)
}
}
.padding(.horizontal, 24)
self.makeDealIcons()
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0))
}
var body: some View {
ZStack {
Color("White").edgesIgnoringSafeArea(.all)
VStack {
self.makeNavigationLink()
List {
self.makeDealHeader()
Section(header: self.makeSegmentedControl()) {
self.makeSection()
}
}
....
UPDATE 2
Here is adaptivePicker
extension View {
func adaptivePicker<Data, ID, Content>(isPresented: Binding<Bool>, selection: Binding<ID>, popoverSize: CGSize? = nil, popoverArrowDirection: UIPopoverArrowDirection = .any, onDone: (() -> Void)? = nil, #ViewBuilder content: #escaping () -> ForEach<Data, ID, Content>) -> some View where Data : RandomAccessCollection, ID: Hashable, Content: View {
self.modifier(AdaptivePicker2(isPresented: isPresented, selection: selection, popoverSize: popoverSize, popoverArrowDirection: popoverArrowDirection, onDone: onDone, content: content))
}
and here is AdaptivePicker2 view modifier implementation
struct AdaptivePicker2<Data, ID, RowContent> : ViewModifier, OrientationAdjustable where Data : RandomAccessCollection, ID: Hashable , RowContent: View {
// MARK: - Environment
#Environment(\.verticalSizeClass) var _verticalSizeClass
var verticalSizeClass: UserInterfaceSizeClass? {
_verticalSizeClass
}
// MARK: - Binding
private var isPresented: Binding<Bool>
private var selection: Binding<ID>
// MARK: - State
#State private var showPicker : Bool = false
// MARK: - Actions
private let onDone: (() -> Void)?
// MARK: - Properties
private let popoverSize: CGSize?
private let popoverArrowDirection: UIPopoverArrowDirection
private let pickerContent: () -> ForEach<Data, ID, RowContent>
// MARK: - Init
init(isPresented: Binding<Bool>, selection: Binding<ID>, popoverSize: CGSize? = nil, popoverArrowDirection: UIPopoverArrowDirection = .any, onDone: (() -> Void)? = nil, #ViewBuilder content: #escaping () -> ForEach<Data, ID, RowContent>) {
self.isPresented = isPresented
self.selection = selection
self.popoverSize = popoverSize
self.popoverArrowDirection = popoverArrowDirection
self.onDone = onDone
self.pickerContent = content
}
var pickerView: some View {
Picker("Select State", selection: self.selection) {
self.pickerContent()
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
func body(content: Content) -> some View {
let isShowingBinding = Binding<Bool>(get: {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withAnimation {
self.showPicker = self.isPresented.wrappedValue
}
}
return self.isPresented.wrappedValue
}, set: {
self.isPresented.wrappedValue = $0
})
let popoverBinding = Binding<Bool>(get: {
self.isPresented.wrappedValue
}, set: {
self.onDone?()
self.isPresented.wrappedValue = $0
})
return Group {
if DeviceType.IS_ANY_IPAD {
if self.popoverSize != nil {
content.presentPopover(isShowing: popoverBinding, popoverSize: popoverSize, arrowDirection: popoverArrowDirection) { self.pickerView }
} else {
content.popover(isPresented: popoverBinding) { self.pickerView }
}
} else {
content.present(isShowing: isShowingBinding) {
ZStack {
Color("Dim")
.opacity(0.25)
.transition(.opacity)
.onTapGesture {
self.isPresented.wrappedValue = false
self.onDone?()
}
VStack {
Spacer()
// TEST: Text("Show Picker: \(self.showPicker ? "True" : "False")")
if self.showPicker {
VStack {
Divider().background(Color.white)
.shadow(color: Color("Dim"), radius: 4)
HStack {
Spacer()
Button("Done") {
print("Tapped picker done button!")
self.isPresented.wrappedValue = false
self.onDone?()
}
.foregroundColor(Color("Accent"))
.padding(.trailing, 16)
}
self.pickerView
.frame(height: self.isLandscape ? 120 : nil)
}
.background(Color.white)
.transition(.move(edge: .bottom))
.animation(.easeInOut(duration: 0.35))
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
}
}
It seems new #StateObject from iOS 14 will solve this issue in SwiftUI.