I created an OTP field using textfield but I want to disable highlighting the text when you double tap or longpress on a TextField.
I tried adjusting the font size to 0. This seems to shrink the grey highlight but it did not hide it totally.
I can't use textSelection(.disabled) because I can only use Xcode 12.3 and this API seems to be available in higher Xcode versions.
I also cannot adjust the frame width of textField to 0 when editing is true because the paste functionality would be disable. Paste to text is a needed requirement.
Here is my code:
import SwiftUI
#available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
var numberOfFields: Int
init(numberOfFields: Int = 6) {
self.numberOfFields = numberOfFields
}
#Published var otpField = "" {
didSet {
guard otpField.last?.isNumber ?? true else {
otpField = oldValue
return
}
if otpField.count == numberOfFields {
hideKeyboard()
}
}
}
#Published var isEditing = false
func otp(digit: Int) -> String {
guard otpField.count >= digit else {
return ""
}
return String(Array(otpField)[digit - 1])
}
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#available(iOS 13.0, *)
struct OTPView: View {
#ObservedObject var viewModel = OTPViewModel()
private let textBoxWidth: CGFloat = 41
private let textBoxHeight = UIScreen.main.bounds.width / 8
private let spaceBetweenLines: CGFloat = 16
private let paddingOfBox: CGFloat = 1
private var textFieldOriginalWidth: CGFloat {
(textBoxWidth + CGFloat(18)) * CGFloat(viewModel.numberOfFields)
}
var body: some View {
VStack {
ZStack {
HStack (spacing: spaceBetweenLines) {
ForEach(1 ... viewModel.numberOfFields, id: \.self) { digit in
otpText(
text: viewModel.otp(digit: digit),
isEditing: viewModel.isEditing,
beforeCursor: digit - 1 < viewModel.otpField.count,
afterCursor: viewModel.otpField.count < digit - 1
)
}
} //: HSTACK
TextField("", text: $viewModel.otpField) { isEditing in
viewModel.isEditing = isEditing
}
.font(Font.system(size: 90, design: .default))
.offset(x: 12, y: 10)
.frame(width: textFieldOriginalWidth, height: textBoxHeight)
.textContentType(.oneTimeCode)
.foregroundColor(.clear)
.background(Color.clear)
.keyboardType(.decimalPad)
.accentColor(.clear)
} //: ZSTACK
} //: VSTACK
}
#available(iOS 13.0, *)
private func otpText(
text: String,
isEditing: Bool,
beforeCursor: Bool,
afterCursor: Bool
) -> some View {
return Text(text)
.font(Font.custom("GTWalsheim-Regular", size: 34))
.frame(width: textBoxWidth, height: textBoxHeight)
.background(VStack{
Spacer()
.frame(height: 65)
ZStack {
Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#BCBEC0"))
Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#367878"))
.offset(x: (beforeCursor ? textBoxWidth : 0) + (afterCursor ? -textBoxWidth : 0))
.animation(.easeInOut, value: [beforeCursor, afterCursor])
.opacity(isEditing ? 1 : 0)
} //: ZSTACK
.clipped()
})
.padding(paddingOfBox)
.accentColor(.clear)
}
}
In the frame of the TextField.
.frame(width: textFieldOriginalWidth, height: textBoxHeight)
Change the width to be reactive to the isEditing state. If true make the width 0 otherwise make it textFieldOriginalWidth
like this:
.frame(width: isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)
Of course this doesn't disable it. But it will not highlight and allow the user to past, copy, etc...
It will have the desired outcome.
Update
To get the OTP to "Autofill" or show up in the keyboard.
Set the Textfield's .textContentType as .oneTimeCode.
The OS should handle the rest read the Apple docs.
Which you have done for textfield.
This action should paste your copied text:
Button(action: paste, label: {
Text("Paste")
})
.
.
.
func paste() {
let pasteboard = UIPasteboard.general
guard let pastedString = pasteboard.string else {
return
}
viewModel.otpField = pastedString
}
The solution is to make the frame of TextField's width equal to 0 when editing but doing this will disable paste functionality so #Alhomaidhi's solution is to add a paste button.
I did this and made it appear when TextField is double or long tap to mimic the iOS clipboard.
Here is the complete code:
import SwiftUI
#available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
var numberOfFields: Int
init(numberOfFields: Int = 6) {
self.numberOfFields = numberOfFields
}
#Published var otpField = "" {
didSet {
showPasteButton = false
guard otpField.last?.isNumber ?? true else {
otpField = oldValue
return
}
if otpField.count == numberOfFields {
hideKeyboard()
showPasteButton = false
}
}
}
#Published var isEditing = false {
didSet {
if !isEditing { showPasteButton = isEditing }
}
}
#Published var showPasteButton = false
func otp(digit: Int) -> String {
guard otpField.count >= digit else {
return ""
}
return String(Array(otpField)[digit - 1])
}
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#available(iOS 13.0, *)
struct CXLRPOTPView: View {
#ObservedObject var viewModel = OTPViewModel()
#Environment(\.colorScheme) var colorScheme
private let textBoxWidth: CGFloat = 41
private let textBoxHeight = UIScreen.main.bounds.width / 8
private let spaceBetweenLines: CGFloat = 16
private let paddingOfBox: CGFloat = 1
private var textFieldOriginalWidth: CGFloat {
(textBoxWidth + CGFloat(18)) * CGFloat(viewModel.numberOfFields)
}
var body: some View {
VStack {
ZStack {
// DOUBLE TAP AND LONG PRESS LISTENER
Text("123456")
.onTapGesture(count: 2) {
viewModel.showPasteButton = true
}
.frame(width: textFieldOriginalWidth, height: textBoxHeight)
.background(Color.clear)
.font(Font.system(size: 90, design: .default))
.foregroundColor(Color.clear)
.onLongPressGesture(minimumDuration: 0.5) {
self.viewModel.showPasteButton = true
}
// OTP TEXT
HStack (spacing: spaceBetweenLines) {
ForEach(1 ... viewModel.numberOfFields, id: \.self) { digit in
otpText(
text: viewModel.otp(digit: digit),
isEditing: viewModel.isEditing,
beforeCursor: digit - 1 < viewModel.otpField.count,
afterCursor: viewModel.otpField.count < digit - 1
)
}
} //: HSTACK
// TEXTFIELD FOR EDITING
TextField("", text: $viewModel.otpField) { isEditing in
viewModel.isEditing = isEditing
}
.font(Font.system(size: 90, design: .default))
.offset(x: 12, y: 10)
.frame(width: viewModel.isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight) // SOLUTION THAT PREVENTED TEXT HIGHLIGHT
.textContentType(.oneTimeCode)
.foregroundColor(.clear)
.background(Color.clear)
.keyboardType(.numberPad)
.accentColor(.clear)
// PASTE BUTTON
Button(action: pasteText, label: {
Text("Paste")
})
.padding(.top, 9)
.padding(.bottom, 9)
.padding(.trailing, 16)
.padding(.leading, 16)
.font(Font.system(size: 14, design: .default))
.accentColor(Color(.white))
.background(Color(colorScheme == .light ? UIColor.black : UIColor.systemGray6))
.cornerRadius(7.0)
.overlay(
RoundedRectangle(cornerRadius: 7).stroke(Color(.black), lineWidth: 2)
)
.opacity(viewModel.showPasteButton ? 1 : 0)
.offset(x: viewModel.numberOfFields >= 6 ? -150 : -100, y: -40)
} //: ZSTACK
} //: VSTACK
}
func pasteText() {
let pasteboard = UIPasteboard.general
guard let pastedString = pasteboard.string else {
return
}
let otpField = pastedString.prefix(viewModel.numberOfFields)
viewModel.otpField = String(otpField)
}
#available(iOS 13.0, *)
private func otpText(
text: String,
isEditing: Bool,
beforeCursor: Bool,
afterCursor: Bool
) -> some View {
return Text(text)
.font(Font.custom("GTWalsheim-Regular", size: 34))
.frame(width: textBoxWidth, height: textBoxHeight)
.background(VStack{
Spacer()
.frame(height: 65)
ZStack {
Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#BCBEC0"))
Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#367878"))
.offset(x: (beforeCursor ? textBoxWidth : 0) + (afterCursor ? -textBoxWidth : 0))
.animation(.easeInOut, value: [beforeCursor, afterCursor])
.opacity(isEditing ? 1 : 0)
} //: ZSTACK
.clipped()
})
.padding(paddingOfBox)
.foregroundColor(Color.black)
}
}
#available(iOS 13.0.0, *)
struct CXLRPOTPView_Previews: PreviewProvider {
static var previews: some View {
CXLRPOTPView(viewModel: OTPViewModel())
.previewLayout(.sizeThatFits)
}
}
Related
I am developing an app with a custom dock and I am unsure of how to change the view when highlighted on an item in the dock. I just couldn't find a way to switch the view when you highlight the item. I have attempted methods such as a switch statement but that did not work in my scenario. I have also attempted to use an if-else statement but that also did not work. I would much appreciate your help in finding a solution to this issue. Please review my code below...
struct MathematicallyController: View {
#State var selection: Int = 1
var body: some View {
ZStack {
ZStack {
if selection == 0 {
//view 1
} else if selection == 1 {
//view 2
} else if selection == 2 {
//view 3
} else {
//view 1
}
}
.overlay(
VStack {
Spacer()
ZStack {
BlurView(style: .systemThinMaterialDark)
.frame(maxWidth: .infinity, maxHeight: 65)
.cornerRadius(20)
.padding()
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 0.9)
.frame(maxWidth: .infinity, maxHeight: 65)
.blur(radius: 2)
.padding()
Picker(selection: selection)
.padding(5)
}
.offset(y: 30)
}
)
}
}
}
Picker
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct BackgroundGeometryReader: View {
var body: some View {
GeometryReader { geometry in
return Color
.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
}
struct SizeAwareViewModifier: ViewModifier {
#Binding private var viewSize: CGSize
init(viewSize: Binding<CGSize>) {
self._viewSize = viewSize
}
func body(content: Content) -> some View {
content
.background(BackgroundGeometryReader())
.onPreferenceChange(SizePreferenceKey.self, perform: { if self.viewSize != $0 { self.viewSize = $0 }})
}
}
struct SegmentedPicker: View {
private static let ActiveSegmentColor: Color = Color(.tertiarySystemBackground)
private static let BackgroundColor: Color = Color(.secondarySystemBackground)
private static let ShadowColor: Color = Color.white.opacity(0.2)
private static let TextColor: Color = Color(.secondaryLabel)
private static let SelectedTextColor: Color = Color(.label)
private static let TextFont: Font = .system(size: 12)
private static let SegmentCornerRadius: CGFloat = 12
private static let ShadowRadius: CGFloat = 10
private static let SegmentXPadding: CGFloat = 16
private static let SegmentYPadding: CGFloat = 9
private static let PickerPadding: CGFloat = 7
private static let AnimationDuration: Double = 0.2
// Stores the size of a segment, used to create the active segment rect
#State private var segmentSize: CGSize = .zero
// Rounded rectangle to denote active segment
private var activeSegmentView: AnyView {
// Don't show the active segment until we have initialized the view
// This is required for `.animation()` to display properly, otherwise the animation will fire on init
let isInitialized: Bool = segmentSize != .zero
if !isInitialized { return EmptyView().eraseToAnyView() }
return
RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius)
.fill(.regularMaterial)
.shadow(color: SegmentedPicker.ShadowColor, radius: SegmentedPicker.ShadowRadius)
.frame(width: self.segmentSize.width, height: self.segmentSize.height)
.offset(x: self.computeActiveSegmentHorizontalOffset(), y: 0)
.animation(Animation.linear(duration: SegmentedPicker.AnimationDuration))
.overlay(
RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius)
.stroke(lineWidth: 1)
.shadow(color: SegmentedPicker.ShadowColor, radius: SegmentedPicker.ShadowRadius)
.frame(width: self.segmentSize.width, height: self.segmentSize.height)
.offset(x: self.computeActiveSegmentHorizontalOffset(), y: 0)
.animation(Animation.linear(duration: SegmentedPicker.AnimationDuration))
)
.eraseToAnyView()
}
#Binding private var selection: Int
private let items: [Image]
init(items: [Image], selection: Binding<Int>) {
self._selection = selection
self.items = items
}
var body: some View {
// Align the ZStack to the leading edge to make calculating offset on activeSegmentView easier
ZStack(alignment: .leading) {
// activeSegmentView indicates the current selection
self.activeSegmentView
HStack {
ForEach(0..<self.items.count, id: \.self) { index in
self.getSegmentView(for: index)
}
}
}
.padding(SegmentedPicker.PickerPadding)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius))
}
// Helper method to compute the offset based on the selected index
private func computeActiveSegmentHorizontalOffset() -> CGFloat {
CGFloat(self.selection) * (self.segmentSize.width + SegmentedPicker.SegmentXPadding / 2)
}
// Gets text view for the segment
private func getSegmentView(for index: Int) -> some View {
guard index < self.items.count else {
return EmptyView().eraseToAnyView()
}
let isSelected = self.selection == index
return
Text(self.items[index])
// Dark test for selected segment
.foregroundColor(isSelected ? SegmentedPicker.SelectedTextColor: SegmentedPicker.TextColor)
.lineLimit(1)
.padding(.vertical, SegmentedPicker.SegmentYPadding)
.padding(.horizontal, SegmentedPicker.SegmentXPadding)
.frame(minWidth: 0, maxWidth: .infinity)
// Watch for the size of the
.modifier(SizeAwareViewModifier(viewSize: self.$segmentSize))
.onTapGesture { self.onItemTap(index: index) }
.eraseToAnyView()
}
// On tap to change the selection
private func onItemTap(index: Int) {
guard index < self.items.count else {
return
}
self.selection = index
}
}
struct Picker: View {
#State var selection: Int = 1
private let items: [Image] = [Image(systemName: "rectangle.on.rectangle"), Image(systemName: "timelapse"), Image(systemName: "plus")]
var body: some View {
SegmentedPicker(items: self.items, selection: self.$selection)
.padding()
}
}
In your Picker struct you are getting selection as a value not a Binding.
The purpose of using a Binding variable is to make the parent of the passed variable listen to the changes made in the struct. In other words, it binds the 2 views/values.
So what you should do is modify Picker like this:
struct Picker: View {
#Binding var selection: Int = 1
private let items: [Image] = [Image(systemName: "rectangle.on.rectangle"), Image(systemName: "timelapse"), Image(systemName: "plus")]
var body: some View {
SegmentedPicker(items: self.items, selection: self.$selection)
.padding()
}
}
And in MathematicallyController change Picker(selection: selection) into Picker(selection: $selection) so you'd have:
struct MathematicallyController: View {
#State var selection: Int = 1
var body: some View {
ZStack {
ZStack {
if selection == 0 {
//view 1
} else if selection == 1 {
//view 2
} else if selection == 2 {
//view 3
} else {
//view 1
}
}
.overlay(
VStack {
Spacer()
ZStack {
BlurView(style: .systemThinMaterialDark)
.frame(maxWidth: .infinity, maxHeight: 65)
.cornerRadius(20)
.padding()
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 0.9)
.frame(maxWidth: .infinity, maxHeight: 65)
.blur(radius: 2)
.padding()
Picker(selection: $selection)
.padding(5)
}
.offset(y: 30)
}
)
}
}
}
Also Note that a switch statement will work just as fine as an if one.
I'm making a math game and I'm trying to implement a circular progress bar, where I get errors. I'm not sure if I'm doing anything wrong, but I can't adapt it to the progress of the game, and that's what I want to do. Please look over my code down below...
struct QuestionView: View {
#State var show = false
#State var showSheet: Bool = false
#State var showSheetA: Bool = false
#State var showSheet2: Bool = false
#State private var correctAnswer = 0
#State private var choiceArray: [Int] = [0, 1, 2, 3]
#State private var firstNumber = 0
#State private var secondNumber = 0
#State private var difficulty = 100
#State private var score = 0
#State private var Background1 = "Background 1"
#State private var Background2 = "Background 2"
#State private var Background3 = "Background 3"
#State private var Background4 = "Background 4"
#State private var Background5 = "Background 5"
#State private var Background6 = "Background 6"
#State private var Background7 = "Background 7"
#State private var Background8 = "Background 8"
#State private var Background9 = "Background 9"
#State private var Background10 = "Background 10"
#State private var Background11 = "Background 11"
#State private var Background12 = "Background 12"
#State private var background = [
"Background 1",
"Background 2",
"Background 3",
"Background 4",
"Background 5",
"Background 6",
"Background 7",
"Background 8",
"Background 9",
"Background 10",
"Background 11",
"Background 12",
]
#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 {
Image("\(Background1)")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.blur(radius: 20)
.onAppear {
shuffle()
}
VStack {
VStack(alignment: .leading) {
HStack {
HStack {
Circle()
//Where the problem is happening at the trim
.trim(from: CGFloat(show ? Int(0.99) : answerIsCorrect(answer: correctAnswer)), to: 0.01)
.stroke(LinearGradient(gradient: Gradient(colors: [Color.white, Color.white.opacity(0.2)]), startPoint: .topLeading, endPoint: .bottomTrailing), style: StrokeStyle(lineWidth: 17.5, lineCap: .round))
.shadow(radius: 8)
.rotationEffect(.degrees(90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(width: 60, height: 60)
.animation(.easeOut)
.padding()
.padding([.top, .leading])
Spacer()
}
Button {
showSheet.toggle()
} label: {
VStack {
ZStack {
Rectangle()
.fill(.thinMaterial)
.frame(width: 80, height: 40)
.cornerRadius(30)
.padding(.top, 13)
.padding(.trailing, 0)
HStack {
Image(systemName: "circle.grid.3x3.fill")
.foregroundColor(.white)
.font(.title3)
.padding(.top, 13)
.padding(.trailing, 0)
}
}
Text("CALCULATOR")
.font(.system(size: 11))
.padding(.trailing, 0)
}
}
.halfSheet(showSheet: $showSheet) {
ZStack {
Color.black.opacity(0.925).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 {
Rectangle()
.fill(.thinMaterial)
.frame(width: 80, height: 40)
.cornerRadius(30)
.padding(.top, 13)
.padding(.trailing, 28.5)
HStack {
Image(systemName: "book")
.foregroundColor(.white)
.font(.title3)
.padding(.top, 13)
.padding(.trailing, 28.5)
}
}
Text("DICTIONARY")
.font(.system(size: 11))
.padding(.trailing, 25.5)
}
}
.halfSheet(showSheet: $showSheet2) {
ZStack {
Home()
}
}
}
.padding(.top, 50)
}
VStack {
Text("Solve the following:")
.foregroundColor(.white)
.font(.title2)
.fontWeight(.medium)
.padding(.bottom, 24)
Text("\(firstNumber) + \(secondNumber)")
.foregroundColor(.white)
.font(.system(size: 60, design: .rounded))
.fontWeight(.medium)
.padding(.bottom, 28)
.onAppear {
generateAnswers()
}
VStack(alignment: .center, spacing: 0.01) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 2.5)]) {
ForEach(0..<4, id: \.self) { index in
Button {
answerIsCorrect(answer: choiceArray[index])
generateAnswers()
} label: {
AnswerButton(number: choiceArray[index])
}
}
}
Spacer()
}
HStack {
Button {
} label: {
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "chevron.left")
.font(.title)
}
}
Button {
showSheetA.toggle()
} label: {
VStack {
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "xmark")
.font(.title)
.foregroundColor(.red)
}
}
}
ZStack {
Rectangle()
.fill(Color.white.opacity(0.5))
.frame(maxWidth: 150, maxHeight: 50)
.cornerRadius(15)
.padding([.leading, .trailing])
Image(systemName: "chevron.right")
.font(.title)
}
}
.padding(.trailing, 6)
.padding(.bottom)
Spacer(minLength: 65)
}
}
}
}
func shuffle() {
Background1 = background[Int.random(in: background.indices)]
Background2 = background[Int.random(in: background.indices)]
Background3 = background[Int.random(in: background.indices)]
Background4 = background[Int.random(in: background.indices)]
Background5 = background[Int.random(in: background.indices)]
Background6 = background[Int.random(in: background.indices)]
Background7 = background[Int.random(in: background.indices)]
Background8 = background[Int.random(in: background.indices)]
Background9 = background[Int.random(in: background.indices)]
Background10 = background[Int.random(in: background.indices)]
Background11 = background[Int.random(in: background.indices)]
Background12 = background[Int.random(in: background.indices)]
}
func answerIsCorrect(answer: Int) {
let isCorrect = answer == correctAnswer ? true : false
if isCorrect {
self.score += 1
} else {
self.score -= 1
}
}
func generateAnswers() {
firstNumber = Int.random(in: 0...(difficulty/2))
secondNumber = Int.random(in: 0...(difficulty/2))
var answerList = [Int]()
correctAnswer = firstNumber + secondNumber
for i in 0...2 {
answerList.append(Int.random(in: 0...difficulty))
}
answerList.append(correctAnswer)
choiceArray = answerList.shuffled()
}
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
}
}
struct AnswerButton: View {
var number: Int
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.white.opacity(0.6))
.frame(maxWidth: .infinity, minHeight: 110)
.cornerRadius(15)
.padding([.leading, .trailing], 10)
.padding(.top)
.padding(.trailing, 6)
Text("\(number)")
.font(.largeTitle)
.fontWeight(.medium)
.foregroundColor(.primary)
.padding([.leading, .trailing], 10)
.padding(.top)
.padding(.trailing, 6)
}
}
}
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
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 = [
.large(),
.medium()
]
presentationController.prefersGrabberVisible = true
}
}
override func viewDidDisappear(_ animated: Bool) {
onDismiss?()
}
}
struct QuestionView_Previews: PreviewProvider {
static var previews: some View {
QuestionView()
}
}
Thank you for looking at my code. I look forward to your answer!
struct ContentView: View {
#State private var progress: Double = 0.1
var body: some View {
VStack {
Circle()
.trim(from: 0, to: progress)
.stroke(lineWidth: 20)
.rotationEffect(Angle(degrees: 180))
.shadow(radius: 8)
.rotationEffect(.degrees(90))
.frame(width: 60, height: 60)
.padding()
Slider(value: $progress, in: 0...1)
.padding()
}
}
}
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
}
}
}
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:
Pressing a button triggers a change to self.value inside PGQuestion1.
SwiftUI re-renders PGQuestion1, and a new closure is passed to .halfSheet { ... } which uses the new value.
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?()
}
}
this bug has been scratching my head for the past few days and I still don't know why the problem is arising and what the fix is. I have a camera screen and integrated it with the TOCropViewController (https://github.com/TimOliver/TOCropViewController) to allow a user to select a picture from their photo library and crop it to show a new post. For some reason the image picker is detecting that it should change the view to the ImagePicker from the camera view screen but it's not displaying it on ios14.4 and below but it works just fine for iOS 14.5 and above.
Here is my camera view code:
struct CameraView: View {
#StateObject var model = CameraModel()
#State var currentZoomFactor: CGFloat = 1.0
#StateObject var registerData = RegisterViewModel()
#StateObject var newPostData = NewPostModel()
enum SheetType {
case imagePick
case imageCrop
case share
}
#State private var currentSheet: SheetType = .imagePick
#State private var actionSheetIsPresented = false
#State private var sheetIsPresented = false
#State private var originalImage: UIImage?
#State private var image: UIImage?
#State private var croppingStyle = CropViewCroppingStyle.default
#State private var croppedRect = CGRect.zero
#State private var croppedAngle = 0
#StateObject var userData = UserViewModel()
var captureButton: some View {
Button(action: {
let impactMed = UIImpactFeedbackGenerator(style: .light)
impactMed.impactOccurred()
model.capturePhoto()
}, label: {
Circle()
.foregroundColor(.white)
.frame(width: 80, height: 80, alignment: .center)
.overlay(
Circle()
.stroke(Color.black.opacity(0.8), lineWidth: 2)
.frame(width: 65, height: 65, alignment: .center)
)
})
}
var capturedPhotoThumbnail: some View {
Group {
RoundedRectangle(cornerRadius: 10)
.frame(width: 55, height: 55, alignment: .center)
.foregroundColor(Color.gray.opacity(0.2))
.onTapGesture(perform: {
// newPostData.picker.toggle()
self.croppingStyle = .default
self.currentSheet = .imagePick
self.sheetIsPresented = true
print("HERE11 and \(self.currentSheet) and \(self.sheetIsPresented)")
})
.overlay(
Image("gallery")
.renderingMode(.template)
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(Color("white")))
//CODE WITH BUG on ios 14.4 and below. I tried a regular sheet as well that works on another view in ios 14.4 but it doesn't work in the cameraview()
.sheet(isPresented: $sheetIsPresented) {
if (self.currentSheet == .imagePick) {
ImagePickerView(croppingStyle: self.croppingStyle, sourceType: .photoLibrary, onCanceled: {
// on cancel
}) { (image) in
guard let image = image else {
return
}
self.originalImage = image
DispatchQueue.main.async {
self.currentSheet = .imageCrop
self.sheetIsPresented = true
}
}
} else if (self.currentSheet == .imageCrop) {
ZStack {
Color("imagecropcolor").edgesIgnoringSafeArea(.all)
ImageCropView(croppingStyle: self.croppingStyle, originalImage: self.originalImage!, onCanceled: {
// on cancel
}) { (image, cropRect, angle) in
// on success
self.image = image
model.resetPhoto()
newPostData.newPost.toggle()
}
}
}
}
}
}
var flipCameraButton: some View {
Button(action: {
let impactMed = UIImpactFeedbackGenerator(style: .light)
impactMed.impactOccurred()
model.flipCamera()
}, label: {
Circle()
.foregroundColor(Color.gray.opacity(0.2))
.frame(width: 45, height: 45, alignment: .center)
.overlay(
Image(systemName: "camera.rotate.fill")
.foregroundColor(.white))
})
}
var body: some View {
GeometryReader { reader in
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
HStack{
Button(action: {
model.switchFlash()
}, label: {
Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
.font(.system(size: 20, weight: .medium, design: .default))
})
.accentColor(model.isFlashOn ? .yellow : .white)
.padding(.leading, 30)
Spacer()
if model.photo != nil {
Text("taken photo").onAppear{
newPostData.newPost.toggle()
}
}
// Image(uiImage: model.photo.image!)
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 60, height: 60)
// .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
// .animation(.spring())
//
}
CameraPreview(session: model.session)
.gesture(
DragGesture().onChanged({ (val) in
// Only accept vertical drag
if abs(val.translation.height) > abs(val.translation.width) {
// Get the percentage of vertical screen space covered by drag
let percentage: CGFloat = -(val.translation.height / reader.size.height)
// Calculate new zoom factor
let calc = currentZoomFactor + percentage
// Limit zoom factor to a maximum of 5x and a minimum of 1x
let zoomFactor: CGFloat = min(max(calc, 1), 5)
// Store the newly calculated zoom factor
currentZoomFactor = zoomFactor
// Sets the zoom factor to the capture device session
model.zoom(with: zoomFactor)
}
})
)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.overlay(
Group {
if model.willCapturePhoto {
Color.black
}
}
)
.animation(.easeInOut)
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
.padding(.trailing, 10)
Spacer()
flipCameraButton
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}.fullScreenCover(isPresented: $newPostData.newPost) {
if model.photo == nil {
NewPost(imageData: (self.image?.pngData())! )
} else {
NewPost(imageData: model.photo.originalData)
}
}
}
}
}
Here is where the CameraView() gets called from my Home Screen
import SwiftUI
import Firebase
struct Home: View {
#AppStorage("current_status") var status = false
#AppStorage("showSheet") var showSheet = false
#State var loadedPost = Post(id: 0, PostUID: "", PostName: "", selectedForPost: false, time: Date())
#State var selectedTab = "camera"
var edges = UIApplication.shared.windows.first?.safeAreaInsets
#StateObject var modelData = ModelView()
#StateObject var userData = UserViewModel()
var body: some View {
VStack(spacing: 15){
VStack (spacing: 0) {
GeometryReader{_ in
ZStack{
if selectedTab == "Post"{
Post(loadedPost: $loadedPost, selectedTab: $selectedTab)
}else if selectedTab == "camera"{
CameraView()
}else if selectedTab == "user"{
User(selectedTab: $selectedTab, loadedPost: $loadedPost)
}
}
}.onChange(of: selectedTab) { (_) in
switch(selectedTab){
case "Post": if
!modelData.isPostLoad{modelData.loadPost()}
case "camera": if
!modelData.isCameraLoad{modelData.loadCamera()}
case "user": if
!modelData.isUserLoad{modelData.loadUser()}
default: ()
}
}
//Tabview hide to show friend modal
if !showSheet{
Divider()
HStack(spacing: 0) {
Spacer(minLength: 0)
TabButton(title: "Post", selectedTab: $selectedTab)
Spacer(minLength: 0)
TabButton(title: "camera", selectedTab: $selectedTab)
.padding(.leading, 30)
.padding(.trailing, 30)
Spacer(minLength: 0)
TabButton(title: "user", selectedTab: $selectedTab)
Spacer(minLength: 0)
}
.padding(.horizontal, 30)
.padding(.bottom, edges!.bottom == 0 ? 15 : edges!.bottom)
.background(Color.black)
}
}
.ignoresSafeArea(.all, edges: .bottom)
.background(Color("Black").ignoresSafeArea(.all, edges: .all))
}
}
}
//Tab Button
struct TabButton : View {
var title: String
#Binding var selectedTab: String
var body: some View {
Button(action: {
withAnimation{selectedTab = title}
}) {
VStack(spacing: 5) {
//Top indicator
//Custom shape...
if title == "user" {
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 26.5, height: 26.5)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}else if title == "camera"{
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 40, height: 40)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}else{
Image(title)
.renderingMode(.template)
.resizable()
.foregroundColor(selectedTab == title ? Color.white : Color("Grey"))
.frame(width: 32.5, height: 32.5)
.padding(.top, UIScreen.screenHeight < 500 ? -5 : 15)
}
}
}
}
}
//can update with load views here
class ModelView: ObservableObject {
#Published var isPostLoad = false
#Published var isCameraLoad = false
#Published var isUserLoad = false
init() {
//load initial data
isCameraLoad = true
print("Home Data Loaded")
}
func loadPost(){
print("Post Loaded")
isPostLoad = true
}
func loadCamera(){
print("Camera Loaded")
isCameraLoad = true
}
func loadUser(){
print("User loaded")
isUserLoad = true
}
}
I would greatly appreciate any help on how to get the ImagePicker view to show up for iOS 14.1-ios 14.4 I've been scratching my head since I worked on it assuming anything that works on iOS 14.5 and above should work on below but only this specific ImagePicker is not working as intended. Thanks!
I cannot center the indicator on which the page is located. I cannot ignore the views on the left and right. When the number of page increases, it is inevitable that the indicator of the page I am on will shift to the left when the indicator number on the right is more than the indicator number on the left.
struct OnboardingView: View {
var pages: [Page] = [
Page(page: PageOne()),
Page(page: PageTwo()),
Page(page: PageThree()),
]
#State var offset: CGFloat = 0
var body: some View {
ScrollView(.init()) {
TabView {
ForEach(pages.indices, id: \.self) { item in
if item == 0 {
AnyView(_fromValue: pages[item].page)
.overlay(
GeometryReader { proxy -> Color in
let minX = proxy.frame(in: .global).minX
DispatchQueue.main.async {
withAnimation(.default) {
self.offset = -minX
}
}
return Color.clear
}
.frame(width: 0, height: 0)
,alignment: .leading
)
} else {
AnyView(_fromValue: pages[item].page)
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.overlay(
HStack(spacing: 15) {
ForEach(pages.indices, id: \.self) { item in
ZStack {
Capsule()
.foregroundColor(Color.white)
.frame(width: getCurrentPageIndex() == item ? 20 : 7, height: 20)
.frame(maxWidth: getCurrentPageIndex() == item ? .infinity : nil, alignment: .center)
}
}
}
.padding(.horizontal)
.padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom)
.padding(.bottom, 10)
,alignment: .bottom
)
}
.ignoresSafeArea()
}
func getCurrentPageIndex() -> Int {
let index = Int(round(Double(offset / getScreenWidth())))
return index
}
func getOffset() -> CGFloat {
let progress = offset / getScreenWidth()
return 22 * progress
}
}
struct OnboardingView_Previews: PreviewProvider {
static var previews: some View {
OnboardingView()
}
}
struct Page: Identifiable {
var id = UUID()
var page: Any
}
extension View {
func getScreenWidth() -> CGFloat {
return UIScreen.main.bounds.width
}
}
.overlay(
HStack(spacing: 15) {
ForEach(pages.indices, id: \.self) { item in
ZStack {
Capsule()
.foregroundColor(Color.white)
.frame(width: getCurrentPageIndex() == item ? 20 : 7, height: 20)
}
}
}
.padding(.horizontal)
.padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom)
.padding(.bottom, 10), alignment: .bottom
)