Home > Blockchain >  SwiftUI Custom Half Modal Calculator Not Working
SwiftUI Custom Half Modal Calculator Not Working

Time:01-05

I'm working on a math game and I would like to implement a small calculator in the background with a custom half modal.

The problem is that the inputted number is not updated unless you physically dismiss the modal and open it again. Although this is an extremely stressful calculator to use, I want to have the text updated immediately after a button is pressed just like a normal calculator (except the fact that it is in a half modal but still).

Please let me know if there is something I'm missing anything and here's my code to review...

struct PGQuestion1: View {
    @State var show = false
    @State var showA = false
    @State var showB = false
    @State var showC = false
    @State var showSheet: Bool = false
    @State var showSheetA: Bool = false
    @State var showSheet2: Bool = false
    
    @State private var chromaShift = false
    
    @State var value = "0"
    let buttons: [[CalcButton]] = [
        [.clear, .negative, .percent, .divide],
        [.seven, .eight, .nine, .multiply],
        [.four, .five, .six, .subtract],
        [.one, .two, .three, .add],
        [.zero, .decimal, .equal]
    ]
    @State var currentOperation: Operation = .none
    @State var runningNumber = 0
    var body: some View {
        ZStack {
            Color.indigo.ignoresSafeArea()
            VStack {
                VStack(alignment: .leading) {
                    HStack {
                        Image(systemName: "1.circle")
                            .foregroundColor(.white)
                            .font(.largeTitle)
                            .padding()
                        Spacer()
                    }
                    
                }
                VStack {
                    Text("Solve the following:")
                        .foregroundColor(.white)
                        .font(.title2)
                        .fontWeight(.medium)
                        .padding(.bottom, 44)
                    Text("(54-9)÷5")
                        .foregroundColor(.white)
                        .font(.system(size: 60, design: .rounded))
                        .fontWeight(.medium)
                        .padding(.bottom, 45)
                    VStack(spacing: 0.01) {
                        ZStack {
                            VStack {
                                if showA {
                                    Rectangle()
                                        .fill(Color.purple.opacity(0.95))
                                        .frame(maxWidth: .infinity, maxHeight: 100)
                                        .cornerRadius(30)
                                        .padding()
                                } else {
                                    Rectangle()
                                        .fill(Color.white.opacity(0.5))
                                        .frame(maxWidth: .infinity, maxHeight: 100)
                                        .cornerRadius(30)
                                        .padding()
                                }
                            }
                            .onTapGesture {
                                showA.toggle()
                                self.showB = false
                                self.showC = false
                            }
                            Text("9")
                                .font(.largeTitle)
                                .foregroundColor(.white)
                        }
                        ZStack {
                            VStack {
                                if showB {
                                    Rectangle()
                                        .fill(Color.purple.opacity(0.95))
                                        .frame(maxWidth: .infinity, maxHeight: 100)
                                        .cornerRadius(30)
                                        .padding()
                                } else {
                                    Rectangle()
                                        .fill(Color.white.opacity(0.5))
                                        .frame(maxWidth: .infinity, maxHeight: 100)
                                        .cornerRadius(30)
                                        .padding()
                                }
                            }
                            .onTapGesture {
                                showB.toggle()
                                self.showA = false
                                self.showC = false
                            }
                            Text("52.2")
                                .font(.largeTitle)
                                .foregroundColor(.white)
                        }
                        HStack(spacing: 20) {
                            Button {
                                showSheet.toggle()
                            } label: {
                                VStack {
                                    ZStack {
                                        Circle()
                                            .fill(Color.purple.opacity(0.8))
                                            .frame(width: 80, height: 80)
                                            .padding(.top, 13)
                                            .padding(.leading, 28.5)
                                        Image(systemName: "circle.grid.3x3.fill")
                                            .foregroundColor(.white)
                                            .font(.largeTitle)
                                            .padding(.top, 13)
                                            .padding(.leading, 28.5)
                                    }
                                    Text("Calculator")
                                        .foregroundColor(.white)
                                        .padding(.bottom)
                                        .padding(.leading, 28.5)
                                }
                            }
                            .halfSheet(showSheet: $showSheet) {
                                ZStack {
                                    Color.black.opacity(0.9).ignoresSafeArea()
                                    VStack {
                                        Spacer()
                                        // Text display
                                        HStack {
                                            Spacer()
                                            Text(value)
                                                .bold()
                                                .font(.system(size: 70))
                                                .foregroundColor(.white)
                                                .minimumScaleFactor(0.5)
                                        }
                                        .padding(.leading)
                                        .padding([.top, .trailing], 23)
                                        .padding(.bottom, 2)
                                        // Our Buttons
                                        ForEach(buttons, id: \.self) { row in
                                            HStack(spacing: 12) {
                                                ForEach(row, id: \.self) { item in
                                                    Button {
                                                        self.didTap(button: item)
                                                    } label: {
                                                        Text(item.rawValue)
                                                            .font(.system(size: 36))
                                                            .frame(width: self.buttonWidth(item: item), height: 55)
                                                            .scaledToFit()
                                                            .background(item.buttonColor)
                                                            .foregroundColor(.white)
                                                            .cornerRadius(95)
                                                    }
                                                }
                                            }
                                            .padding(.bottom, 0.55)
                                            .padding([.leading, .trailing], 20)
                                        }
                                    }
                                }
                            }
                            Button {
                                showSheet2.toggle()
                            } label: {
                                VStack {
                                    ZStack {
                                        Circle()
                                            .fill(Color.purple.opacity(0.8))
                                            .frame(width: 80, height: 80)
                                            .padding(.top, 13)
                                        Image(systemName: "book")
                                            .foregroundColor(.white)
                                            .font(.largeTitle)
                                            .padding(.top, 13)
                                    }
                                    Text("Dictonary")
                                        .foregroundColor(.white)
                                        .padding(.bottom)
                                }
                            }
                            .halfSheet(showSheet: $showSheet2) {
                                ZStack {
                                    Color.black.opacity(0.9).ignoresSafeArea()
                                    VStack(alignment: .leading) {
                                        HStack {
                                            
                                        }
                                    }
                                }
                            }
                            VStack {
                                if !show {
                                    ZStack {
                                        Circle()
                                            .fill(Color.purple.opacity(0.8))
                                            .frame(width: 80, height: 80)
                                            .blur(radius: 10)
                                            .padding(.top, 13)
                                            .padding(.trailing)
                                        Image(systemName: "info")
                                            .foregroundColor(.black)
                                            .font(.largeTitle)
                                            .padding(.top, 13)
                                            .padding(.trailing)
                                    }
                                    .onTapGesture {
                                        withAnimation {
                                            show.toggle()
                                        }
                                    }
                                } else {
                                    ZStack {
                                        Circle()
                                            .fill(Color.purple.opacity(0.8))
                                            .frame(width: 80, height: 80)
                                            .blur(radius: 10.35)
                                            .padding(.top, 13)
                                            .padding(.trailing)
                                            Text("Upgrade to Mathematically MAX")
                                                .foregroundColor(.black)
                                                .font(.system(size: 13))
                                                .minimumScaleFactor(0.5)
                                                .multilineTextAlignment(.center)
                                                .padding(.top, 16)
                                                .padding(1)
                                                .padding(4)
                                                .padding(5)
                                                .padding(.trailing)
                                    }
                                    .onTapGesture {
                                        withAnimation {
                                            show.toggle()
                                        }
                                    }
                                }
                                Text("Intelligent Finding")
                                    .foregroundColor(.black)
                                    .font(.system(size: 12))
                                    .padding(.top, 2)
                                    .padding(.bottom)
                                    .padding(.trailing)
                            }
                        }
                        .frame(maxWidth: .infinity, maxHeight: 140)
                        .background(Color.white.opacity(0.3))
                        .cornerRadius(40)
                        .padding()
                    }
                }
                HStack {
                    ZStack {
                        Text("Swipe to the next page")
                            .font(.title2)
                            .foregroundColor(Color.yellow)
                            .shadow(color: .white, radius: 10)
                            .hueRotation(.degrees(chromaShift ? 0 : 520))
                            .animation(Animation.linear(duration: 4).repeatForever(autoreverses: true))
                            .onAppear() {
                                self.chromaShift.toggle()
                            }
                    }
                    Image(systemName: "chevron.forward")
                        .font(.title2)
                        .foregroundColor(Color.yellow)
                        .shadow(color: .white, radius: 10)
                        .hueRotation(.degrees(chromaShift ? 0 : 520))
                        .animation(Animation.linear(duration: 4).repeatForever(autoreverses: true))
                        .onAppear() {
                            self.chromaShift.toggle()
                        }
                }
                Spacer()
            }
        }
    }
    func didTap(button: CalcButton) {
        switch button {
        case .add, .subtract, .multiply, .divide, .equal:
            if button == .add {
                self.currentOperation = .add
                self.runningNumber = Int(self.value) ?? 0
            } else if button == .subtract {
                self.currentOperation = .subtract
                self.runningNumber = Int(self.value) ?? 0
            } else if button == .multiply {
                self.currentOperation = .multiply
                self.runningNumber = Int(self.value) ?? 0
            } else if button == .divide {
                self.currentOperation = .divide
                self.runningNumber = Int(self.value) ?? 0
            } else if button == .equal {
                let runningValue = self.runningNumber
                let currentValue = Int(self.value) ?? 0
                switch self.currentOperation {
                case .add:
                    self.value = "\(runningValue   currentValue)"
                case .subtract:
                    self.value = "\(runningValue - currentValue)"
                case .multiply:
                    self.value = "\(runningValue * currentValue)"
                case .divide:
                    self.value = "\(runningValue / currentValue)"
                case .none:
                    break
                }
            }
            if button != .equal {
                self.value = "0"
            }
        case .clear:
            self.value = "0"
        case .decimal, .percent, .negative:
            break
        default:
            let number = button.rawValue
            if self.value == "0" {
                value = number
            } else {
                self.value = "\(self.value)\(number)"
            }
        }
    }
    func buttonWidth(item: CalcButton) -> CGFloat {
        if item == .zero {
            return ((UIScreen.main.bounds.width - (4*12)) / 4) * 2
        }
        return (UIScreen.main.bounds.width - (5*12)) / 4
    }
    func buttonHeight() -> CGFloat {
        return (UIScreen.main.bounds.height - (5*12)) / 4
    }
}

enum CalcButton: String {
    case one = "1"
    case two = "2"
    case three = "3"
    case four = "4"
    case five = "5"
    case six = "6"
    case seven = "7"
    case eight = "8"
    case nine = "9"
    case zero = "0"
    case add = " "
    case subtract = "-"
    case divide = "÷"
    case multiply = "×"
    case equal = "="
    case clear = "AC"
    case decimal = "."
    case percent = "%"
    case negative = " /-"
    
    var buttonColor: Color {
        switch self {
        case .add, .subtract, .multiply, .divide, .equal:
            return .orange
        case .clear, .negative, .percent:
            return .gray
        default:
            return Color(UIColor(red: 55/255.0, green: 55/255.0, blue: 55/255.0, alpha: 1))
        }
    }
}

enum Operation {
    case add, subtract, multiply, divide, none
}

extension View {
    func halfSheet<SheetView: View>(showSheet: Binding<Bool>, @ViewBuilder sheetView: @escaping () -> SheetView) -> some View {
        return self
            .background(
                HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet)
            )
    }
}

struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
    var sheetView: SheetView
    @Binding var showSheet: Bool
    let controller = UIViewController()
    func makeUIViewController(context: Context) -> UIViewController {
        controller.view.backgroundColor = .clear
        return controller
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let sheetController = CustomHostingController(rootView: sheetView)
        if showSheet {
            uiViewController.present(sheetController, animated: true) {
                DispatchQueue.main.async {
                    self.showSheet.toggle()
                }
            }
        }
    }
}

class CustomHostingController<Content: View>: UIHostingController<Content> {
    override func viewDidLoad() {
        if let presentationController = presentationController as? UISheetPresentationController {
            presentationController.detents = [
                .medium(),
                .large()
            ]
            presentationController.prefersGrabberVisible = true
        }
    }
}

CodePudding user response:

The problem is in your func halfSheet() and struct HalfSheetHelper. You have created a UIViewControllerRepresentable view, but it does not handle updates.

Once the sheet has already been shown, the sequence of events happening is:

  1. Pressing a button triggers a change to self.value inside PGQuestion1.
  2. SwiftUI re-renders PGQuestion1, and a new closure is passed to .halfSheet { ... } which uses the new value.
  3. Since the HalfSheetHelper is UIViewRepresentable, SwiftUI calls your updateUIViewController() function. A completely new CustomHostingController is created. Then nothing else happens because showSheet is false.

To handle updates properly, I recommend you create a Coordinator class inside your HalfSheetHelper. The coordinator is a class that SwiftUI will keep alive as long as the view is being used, and it can maintain a persistent reference to the CustomHostingController. Then in updateUIViewController(), you can use the coordinator to access the hosting controller and re-assign its rootView with the newly updated sheet contents. For more on these techniques, see the Interfacing with UIKit tutorial.

I also changed how showSheet is handled so that it becomes false only once the sheet is dismissed. This required adding a custom onDismiss closure to the CustomHostingController which it calls in viewDidDisappear.

(There still seems to be a bug with the half-sheet, where if I swipe down to dismiss the sheet and then show it again, it appears fullscreen instead of half-screen. I'm not familiar enough with the presentationController/detents APIs to figure out why this is happening!)

struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
  var sheetView: SheetView
  @Binding var showSheet: Bool

  class Coordinator {
    let dummyController = UIViewController()
    let sheetController: CustomHostingController<SheetView>
    init(sheetView: SheetView, showSheet: Binding<Bool>) {
      sheetController = CustomHostingController(rootView: sheetView, onDismiss: { showSheet.wrappedValue = false })
      dummyController.view.backgroundColor = .clear
    }
  }
  func makeCoordinator() -> Coordinator {
    return Coordinator(sheetView: sheetView, showSheet: $showSheet)
  }
  func makeUIViewController(context: Context) -> UIViewController {
    return context.coordinator.dummyController
  }
  func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    context.coordinator.sheetController.rootView = sheetView

    if showSheet && uiViewController.presentedViewController == nil {
      uiViewController.present(context.coordinator.sheetController, animated: true)
    }
  }
}


class CustomHostingController<Content: View>: UIHostingController<Content> {
  var onDismiss: (() -> Void)?
  convenience init(rootView: Content, onDismiss: @escaping () -> Void) {
    self.init(rootView: rootView)
    self.onDismiss = onDismiss
  }

  override func viewDidLoad() {
    if let presentationController = presentationController as? UISheetPresentationController {
      presentationController.detents = [
        .medium(),
        .large()
      ]
      presentationController.prefersGrabberVisible = true
    }
  }

  override func viewDidDisappear(_ animated: Bool) {
    onDismiss?()
  }
}

CodePudding user response:

to make the value show in the halfSheet Text, I used the code at: https://www.keaura.com/blog/sheets-with-swiftui instead of the halfSheet you provided. Also, I "attached" this new halfSheet to the View ZStack, like this:

var body: some View {
    ZStack {
    ....
    }
    .halfSheet(isPresented: $showSheet)  { ... }
    .halfSheet(isPresented: $showSheet2) { ... }
  •  Tags:  
  • Related