Creating OTP page for SwiftUI Using TextField - swiftui

I am trying to create an OTP page for my app but I don't know how to make the next textfield focus after I input a single digit in the first text field.
I created 6 text field for each digit of OTP. The next text field should be the first responder once I key in one digit from the first text field and so forth untill all 6 digits are complete.
I'm not sure how to do that in Swift UI. So far I manage to create 6 lines only as seen in the screenshot. The expected is only one digit should be per line. So the next text field should be focus once I input a single integer.
I tried other post like the use of #FocusState but it says unknown attribute.
I also tried the custom text field How to move to next TextField in SwiftUI?
but I cannot seem to make it work.
import SwiftUI
struct ContentView: View {
#State private var OTP1 = ""
#State private var OTP2 = ""
#State private var OTP3 = ""
#State private var OTP4 = ""
#State private var OTP5 = ""
#State private var OTP6 = ""
var body: some View {
VStack {
HStack(spacing: 16) {
VStack {
TextField("", text: $OTP1)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP2)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP3)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP4)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP5)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP6)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
}
}
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 560, height: 50))
}
}
My OTP Page
Expected field

Here is my answer for iOS 14.
The view.
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var isFocused = false
let textBoxWidth = UIScreen.main.bounds.width / 8
let textBoxHeight = UIScreen.main.bounds.width / 8
let spaceBetweenBoxes: CGFloat = 10
let paddingOfBox: CGFloat = 1
var textFieldOriginalWidth: CGFloat {
(textBoxWidth*6)+(spaceBetweenBoxes*3)+((paddingOfBox*2)*3)
}
var body: some View {
VStack {
ZStack {
HStack (spacing: spaceBetweenBoxes){
otpText(text: viewModel.otp1)
otpText(text: viewModel.otp2)
otpText(text: viewModel.otp3)
otpText(text: viewModel.otp4)
otpText(text: viewModel.otp5)
otpText(text: viewModel.otp6)
}
TextField("", text: $viewModel.otpField)
.frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight)
.disabled(viewModel.isTextFieldDisabled)
.textContentType(.oneTimeCode)
.foregroundColor(.clear)
.accentColor(.clear)
.background(Color.clear)
.keyboardType(.numberPad)
}
}
}
private func otpText(text: String) -> some View {
return Text(text)
.font(.title)
.frame(width: textBoxWidth, height: textBoxHeight)
.background(VStack{
Spacer()
RoundedRectangle(cornerRadius: 1)
.frame(height: 0.5)
})
.padding(paddingOfBox)
}
}
This is the viewModel.
class ViewModel: ObservableObject {
#Published var otpField = "" {
didSet {
guard otpField.count <= 6,
otpField.last?.isNumber ?? true else {
otpField = oldValue
return
}
}
}
var otp1: String {
guard otpField.count >= 1 else {
return ""
}
return String(Array(otpField)[0])
}
var otp2: String {
guard otpField.count >= 2 else {
return ""
}
return String(Array(otpField)[1])
}
var otp3: String {
guard otpField.count >= 3 else {
return ""
}
return String(Array(otpField)[2])
}
var otp4: String {
guard otpField.count >= 4 else {
return ""
}
return String(Array(otpField)[3])
}
var otp5: String {
guard otpField.count >= 5 else {
return ""
}
return String(Array(otpField)[4])
}
var otp6: String {
guard otpField.count >= 6 else {
return ""
}
return String(Array(otpField)[5])
}
#Published var borderColor: Color = .black
#Published var isTextFieldDisabled = false
var successCompletionHandler: (()->())?
#Published var showResendText = false
}
Not very reusable but it works....
If you want to change the length don't forget to update the viewModel's otpField's didSet and the views textFieldOriginalWidth.
The idea here is to hide the TextField and make it seem like the user is typing in the boxes.
An Idea could be to shrink the TextField when user is typing by using the isEditing closure from the TextField. You would want to shrink it so the user can't paste text or get that "popup" or the textfield cursor.

Related

Calculator Functionality

I’m working on a dilution calculator. I have it 98% working, however, I want it to work a certain way and I’m not sure to do that. This is my first app so I’m new at this.
So I want the user to be able to input the numbers and hit a button to get the calculation. I’ve been using #State and through my research and understanding, using that instantly updates any changes the user makes.
So how do I go about making the app wait till the user hits the “Go” button.
Hers my code so far.
#State private var ContainerSize = 0
#State private var DilutionRatio = 0
#State private var Go = ""
#State private var TotalProduct = 0.0
#State private var TotalWater = 0.0
#FocusState private var amountIsFocused: Bool
var totalProductAmount: Double {
let firstValue = Double(ContainerSize)
let secondValue = Double(DilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
return totalProduct
}
var totalWaterAmount: Double {
let firstValue = Double(ContainerSize)
let secondValue = Double(DilutionRatio + 1)
let totalWater = Double(firstValue - secondValue)
return totalWater
}
//Container Size
ZStack {
Image("Container Size (Oz)")
.padding(.vertical, -15)
TextField("", value: $ContainerSize, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
}
}
//Dilution Ratio
ZStack {
Image("Dilution Ratio - 2")
.padding(.vertical, -10)
TextField("", value: $DilutionRatio, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
}
//Go Button
Button(action: {}, label: {
Image("Go Button")
})
//Results
HStack{
ZStack {
Image("Total Product (Oz)")
Text("\(totalProductAmount, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
}
ZStack {
Image("Total Water (Oz)")
Text(totalWaterAmount, format: .number)
.font(Font.system(size: 60, design: .default))
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer(
Button("Done") {
amountIsFocused = false
}
}
struct CalculatorIView_Previews: PreviewProvider {
static var previews: some View {
CalculatorIView()
}
}
The calculator works as is but I want the user to input numbers, hit the “Go” button, and the results are shown.
You can create func for calculation and call it from button action. You should remove calculated properties, var totalProductAmount: Double and var totalWaterAmount: Double and do the calculation inside the function. You can check the example below.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-tappable-button
var body: some View{
Button(action: {
someCalculation()
}, label: {
Image("Go Button")
})
}
func someCalculation(){
// do some calculation and you can set #State variables or you can return some value. For example 'func someCalculation()->Double'
}
So far you do the calculation in calculated properties and display them directly. So they'll update every time one of their underlying #State values change.
If you only want to show results on button press, you should display your #State result vars, and update inside the button action.
Side note: property names should start lowercase.
struct ContentView: View {
// side note: var names should start lowerCase
#State private var containerSize = 0
#State private var dilutionRatio = 0
#State private var totalProduct = 0.0
#State private var totalWater = 0.0
// for clarity change calculations to funcs
func totalProductAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
return totalProduct
}
func totalWaterAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalWater = Double(firstValue - secondValue)
return totalWater
}
var body: some View {
VStack {
//Container Size
Text("Container Size (Oz)")
.padding(.vertical, -15)
TextField("", value: $containerSize, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
//Dilution Ratio
Text("Dilution Ratio - 2")
.padding(.vertical, -10)
TextField("", value: $dilutionRatio, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
//Go Button
Button(action: {
// Calculate here, and set State vars with results
totalProduct = totalProductAmount()
totalWater = totalWaterAmount()
}, label: {
Text("Go Button")
})
.buttonStyle(.borderedProminent)
.padding()
//Results
// Show the state vars, not the calculation vars!
HStack{
VStack {
Text("Total Product (Oz)")
Text("\(totalProduct, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
}
VStack {
Text("Total Water (Oz)")
Text(totalWater, format: .number)
.font(Font.system(size: 60, design: .default))
}
}
}
}
}

Implementing a tag list in SwiftUI

I am trying to implement a tag list in SwiftUI but I'm unsure how to get it to wrap the tags to additional lines if the list overflows horizontally. I started with a string array called tags and within SwiftUI I loop through the array and create buttons as follows:
HStack{
ForEach(tags, id: \.self){tag in
Button(action: {}) {
HStack {
Text(tag)
Image(systemName: "xmark.circle")
}
}
.padding()
.foregroundColor(.white)
.background(Color.orange)
.cornerRadius(.infinity)
.lineLimit(1)
}
}
If the tags array is small it renders as follows:
However, if the array has more values it does this:
The behavior I am looking for is for the last tag (yellow) to wrap to the second line. I realize it is in an HStack, I was hoping I could add a call to lineLimit with a value of greater than one but it doesn't seem to change the behavior. If I change the outer HStack to a VStack, it puts each Button on a separate line, so still not quite the behavior I am trying create. Any guidance would be greatly appreciated.
Federico Zanetello shared a nice solution in his blog: Flexible layouts in SwiftUI.
The solution is a custom view called FlexibleView which computes the necessary Row's and HStack's to lay down the given elements and wrap them into multiple rows if needed.
struct _FlexibleView<Data: Collection, Content: View>: View where Data.Element: Hashable {
let availableWidth: CGFloat
let data: Data
let spacing: CGFloat
let alignment: HorizontalAlignment
let content: (Data.Element) -> Content
#State var elementsSize: [Data.Element: CGSize] = [:]
var body : some View {
VStack(alignment: alignment, spacing: spacing) {
ForEach(computeRows(), id: \.self) { rowElements in
HStack(spacing: spacing) {
ForEach(rowElements, id: \.self) { element in
content(element)
.fixedSize()
.readSize { size in
elementsSize[element] = size
}
}
}
}
}
}
func computeRows() -> [[Data.Element]] {
var rows: [[Data.Element]] = [[]]
var currentRow = 0
var remainingWidth = availableWidth
for element in data {
let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)]
if remainingWidth - (elementSize.width + spacing) >= 0 {
rows[currentRow].append(element)
} else {
currentRow = currentRow + 1
rows.append([element])
remainingWidth = availableWidth
}
remainingWidth = remainingWidth - (elementSize.width + spacing)
}
return rows
}
}
Usage:
FlexibleView(
data: [
"Here’s", "to", "the", "crazy", "ones", "the", "misfits", "the", "rebels", "the", "troublemakers", "the", "round", "pegs", "in", "the", "square", "holes", "the", "ones", "who", "see", "things", "differently", "they’re", "not", "fond", "of", "rules"
],
spacing: 15,
alignment: .leading
) { item in
Text(verbatim: item)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.2))
)
}
.padding(.horizontal, model.padding)
}
Full code available at https://github.com/zntfdr/FiveStarsCodeSamples.
Ok, this is my first answer on this site, so bear with me if I commit some kind of stack overflow faux pas.
I'll post my solution, which works for a model where the tags are either present in a selectedTags set or not, and all available tags are present in an allTags set. In my solution, these are set as bindings, so they can be injected from elsewhere in the app. Also, my solution has the tags ordered alphabetically because that was easiest. If you want them ordered a different way, you'll probably need to use a different model than two independent sets.
This definitely won't work for everyone's use case, but since I couldn't find my own answer for this out there, and your question was the only place I could find mentioning the idea, I decided I would try to build something that would work for me and share it with you. Hope it helps:
struct TagList: View {
#Binding var allTags: Set<String>
#Binding var selectedTags: Set<String>
private var orderedTags: [String] { allTags.sorted() }
private func rowCounts(_ geometry: GeometryProxy) -> [Int] { TagList.rowCounts(tags: orderedTags, padding: 26, parentWidth: geometry.size.width) }
private func tag(rowCounts: [Int], rowIndex: Int, itemIndex: Int) -> String {
let sumOfPreviousRows = rowCounts.enumerated().reduce(0) { total, next in
if next.offset < rowIndex {
return total + next.element
} else {
return total
}
}
let orderedTagsIndex = sumOfPreviousRows + itemIndex
guard orderedTags.count > orderedTagsIndex else { return "[Unknown]" }
return orderedTags[orderedTagsIndex]
}
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
ForEach(0 ..< self.rowCounts(geometry).count, id: \.self) { rowIndex in
HStack {
ForEach(0 ..< self.rowCounts(geometry)[rowIndex], id: \.self) { itemIndex in
TagButton(title: self.tag(rowCounts: self.rowCounts(geometry), rowIndex: rowIndex, itemIndex: itemIndex), selectedTags: self.$selectedTags)
}
Spacer()
}.padding(.vertical, 4)
}
Spacer()
}
}
}
}
struct TagList_Previews: PreviewProvider {
static var previews: some View {
TagList(allTags: .constant(["one", "two", "three"]), selectedTags: .constant(["two"]))
}
}
extension String {
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
}
extension TagList {
static func rowCounts(tags: [String], padding: CGFloat, parentWidth: CGFloat) -> [Int] {
let tagWidths = tags.map{$0.widthOfString(usingFont: UIFont.preferredFont(forTextStyle: .headline))}
var currentLineTotal: CGFloat = 0
var currentRowCount: Int = 0
var result: [Int] = []
for tagWidth in tagWidths {
let effectiveWidth = tagWidth + (2 * padding)
if currentLineTotal + effectiveWidth <= parentWidth {
currentLineTotal += effectiveWidth
currentRowCount += 1
guard result.count != 0 else { result.append(1); continue }
result[result.count - 1] = currentRowCount
} else {
currentLineTotal = effectiveWidth
currentRowCount = 1
result.append(1)
}
}
return result
}
}
struct TagButton: View {
let title: String
#Binding var selectedTags: Set<String>
private let vPad: CGFloat = 13
private let hPad: CGFloat = 22
private let radius: CGFloat = 24
var body: some View {
Button(action: {
if self.selectedTags.contains(self.title) {
self.selectedTags.remove(self.title)
} else {
self.selectedTags.insert(self.title)
}
}) {
if self.selectedTags.contains(self.title) {
HStack {
Text(title)
.font(.headline)
}
.padding(.vertical, vPad)
.padding(.horizontal, hPad)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(radius)
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(Color(UIColor.systemBackground), lineWidth: 1)
)
} else {
HStack {
Text(title)
.font(.headline)
.fontWeight(.light)
}
.padding(.vertical, vPad)
.padding(.horizontal, hPad)
.foregroundColor(.gray)
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(Color.gray, lineWidth: 1)
)
}
}
}
}
I found this gist which once built, looks amazing! It did exactly what I needed for making and deleting tags. Here is a sample I built for a multi platform swift app from the code.
Tagger View
struct TaggerView: View {
#State var newTag = ""
#State var tags = ["example","hello world"]
#State var showingError = false
#State var errorString = "x" // Can't start empty or view will pop as size changes
var body: some View {
VStack(alignment: .leading) {
ErrorMessage(showingError: $showingError, errorString: $errorString)
TagEntry(newTag: $newTag, tags: $tags, showingError: $showingError, errorString: $errorString)
TagList(tags: $tags)
}
.padding()
.onChange(of: showingError, perform: { value in
if value {
// Hide the error message after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
showingError = false
}
}
})
}
}
ErrorMessage View
struct ErrorMessage: View {
#Binding var showingError: Bool
#Binding var errorString: String
var body: some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text(errorString)
.foregroundColor(.secondary)
.padding(.leading, -6)
}
.font(.caption)
.opacity(showingError ? 1 : 0)
.animation(.easeIn(duration: 0.3), value: showingError)
}
}
TagEntry View
struct TagEntry: View {
#Binding var newTag: String
#Binding var tags: [String]
#Binding var showingError: Bool
#Binding var errorString: String
var body: some View {
HStack {
TextField("Add Tags", text: $newTag, onCommit: {
addTag(newTag)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
Image(systemName: "plus.circle")
.foregroundColor(.blue)
.onTapGesture {
addTag(newTag)
}
}
.onChange(of: newTag, perform: { value in
if value.contains(",") {
// Try to add the tag if user types a comma
newTag = value.replacingOccurrences(of: ",", with: "")
addTag(newTag)
}
})
}
/// Checks if the entered text is valid as a tag. Sets the error message if it isn't
private func tagIsValid(_ tag: String) -> Bool {
// Invalid tags:
// - empty strings
// - tags already in the tag array
let lowerTag = tag.lowercased()
if lowerTag == "" {
showError(.Empty)
return false
} else if tags.contains(lowerTag) {
showError(.Duplicate)
return false
} else {
return true
}
}
/// If the tag is valid, it is added to an array, otherwise the error message is shown
private func addTag(_ tag: String) {
if tagIsValid(tag) {
tags.append(newTag.lowercased())
newTag = ""
}
}
private func showError(_ code: ErrorCode) {
errorString = code.rawValue
showingError = true
}
enum ErrorCode: String {
case Empty = "Tag can't be empty"
case Duplicate = "Tag can't be a duplicate"
}
}
TagList View
struct TagList: View {
#Binding var tags: [String]
var body: some View {
GeometryReader { geo in
generateTags(in: geo)
.padding(.top)
}
}
/// Adds a tag view for each tag in the array. Populates from left to right and then on to new rows when too wide for the screen
private func generateTags(in geo: GeometryProxy) -> some View {
var width: CGFloat = 0
var height: CGFloat = 0
return ZStack(alignment: .topLeading) {
ForEach(tags, id: \.self) { tag in
Tag(tag: tag, tags: $tags)
.alignmentGuide(.leading, computeValue: { tagSize in
if (abs(width - tagSize.width) > geo.size.width) {
width = 0
height -= tagSize.height
}
let offset = width
if tag == tags.last ?? "" {
width = 0
} else {
width -= tagSize.width
}
return offset
})
.alignmentGuide(.top, computeValue: { tagSize in
let offset = height
if tag == tags.last ?? "" {
height = 0
}
return offset
})
}
}
}
}
Tag View
struct Tag: View {
var tag: String
#Binding var tags: [String]
#State var fontSize: CGFloat = 20.0
#State var iconSize: CGFloat = 20.0
var body: some View {
HStack {
Text(tag.lowercased())
.font(.system(size: fontSize, weight: .regular, design: .rounded))
.padding(.leading, 2)
Image(systemName: "xmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.red, .blue, .white)
.font(.system(size: iconSize, weight: .black, design: .rounded))
.opacity(0.7)
.padding(.leading, -5)
}
.foregroundColor(.white)
.font(.caption2)
.padding(4)
.background(Color.blue.cornerRadius(5))
.padding(4)
.onTapGesture {
tags = tags.filter({ $0 != tag })
}
}
}
And finally…
Context View
import SwiftUI
struct ContentView: View {
var body: some View {
TaggerView()
}
}
I can’t take any credit for the code but let me send a huge thanks to Alex Hay for creating and posting this.
Link to the gist code on GitHub
I hope this helps someone.

How to create Radiobuttons in SwiftUI?

I would like to react on a choice of a user. Something similar to this example:
In a 2nd stage would I like to show additional content below each radiobutton, e.g. moving the buttons 2 and 3 from each other in order to give a list of websites for allowing.
So far I haven't found how to do this in SwiftUI.
Many thanks in advance!
Picker(selection: $order.avocadoStyle, label: Text("Avocado:")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}.pickerStyle(RadioGroupPickerStyle())
This is the code from the 2019 swiftUI essentials keynote (SwiftUI Essentials - WWDC 2019. Around 43 minutes in the video they show this example.
It will look like this:
check this out...an easy to use SwiftUI RadiobuttonGroup for iOS
you can use it like this:
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
and here is the code:
struct ColorInvert: ViewModifier {
#Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
Group {
if colorScheme == .dark {
content.colorInvert()
} else {
content
}
}
}
}
struct RadioButton: View {
#Environment(\.colorScheme) var colorScheme
let id: String
let callback: (String)->()
let selectedID : String
let size: CGFloat
let color: Color
let textSize: CGFloat
init(
_ id: String,
callback: #escaping (String)->(),
selectedID: String,
size: CGFloat = 20,
color: Color = Color.primary,
textSize: CGFloat = 14
) {
self.id = id
self.size = size
self.color = color
self.textSize = textSize
self.selectedID = selectedID
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.selectedID == self.id ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.size, height: self.size)
.modifier(ColorInvert())
Text(id)
.font(Font.system(size: textSize))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(self.color)
}
}
struct RadioButtonGroup: View {
let items : [String]
#State var selectedId: String = ""
let callback: (String) -> ()
var body: some View {
VStack {
ForEach(0..<items.count) { index in
RadioButton(self.items[index], callback: self.radioGroupCallback, selectedID: self.selectedId)
}
}
}
func radioGroupCallback(id: String) {
selectedId = id
callback(id)
}
}
struct ContentView: View {
var body: some View {
HStack {
Text("Example")
.font(Font.headline)
.padding()
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentViewDark_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
.darkModeFix()
}
}
I just edited #LizJ answer , by adding Binding instead of didTapActive & didTapInactive , so like that it will looks like other SwiftUI elements
import SwiftUI
struct RadioButton: View {
#Binding var checked: Bool //the variable that determines if its checked
var body: some View {
Group{
if checked {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.checked = false}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.checked = true}
}
}
}
}
I'm using swift4, Catalina OS and Xcode 11.2 and was having the issue where RadioGroupPickerStyle was unavailable for iOS and .radiogroup just didn't work (it froze in build) so I made my own that's reusable for other occasions. (notice its only the button so you have to handle the logic yourself.) Hope it helps!
import SwiftUI
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
}
}
}
}
TO USE: Put this in any file and you can use it as you would any other view anywhere else in the project. (we keep a global folder that has a buttons file in it)
I will use the previous answer of #LizJ and i will add a text after the radio button to resemble (RadioListTile in Flutter)
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let radioTitle: String
var onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
HStack(alignment: .center, spacing: 16) {
ZStack{
Circle()
.fill(AppColors.primaryColor)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
Text(radioTitle)
.font(.headline)
}
} else {
HStack(alignment: .center, spacing: 16){
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
Text(radioTitle)
.font(.headline)
}
}
}
}
I will also provide an example for the selection logic
we will create a enum for radio cases
enum PaymentMethod: Int {
case undefined = 0
case credit = 1
case cash = 2
}
then we will create #State variable to carry the selection, i will not recreate another SwiftUI view but only explain the basic concept without any boilerplate code
struct YourView: View {
#State private var paymentMethod: PaymentMethod
var body: some View {
RadioButton(ifVariable: paymentMethod == PaymentMethod.credit,radioTitle: "Pay in Credit", onTapToActive: {
paymentMethod = .credit
}, onTapToInactive: {})
RadioButton(ifVariable: paymentMethod == PaymentMethod.cash,radioTitle: "Pay in Cash", onTapToActive: {
paymentMethod = .cash
}, onTapToInactive: {})
}
}
with this previous code you can toggle between radio buttons in SwiftUI with a text after each selection to resemble (RadioListTile in Flutter)

'Modifying state during view update, this will cause undefined behavior.' error when typing on a textfield (SwiftUI)

I have two textfields, assigned to:
#State private var emailAddress: String = ""
#State private var password: String = ""
Now whenever I am typing on it, the app seems to get stuck and gives me this error:
'Modifying state during view update, this will cause undefined behavior.'
I have a StartView():
class UserSettings: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
didSet {
didChange.send(())
}
}
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(TabbarView())
}else {
return AnyView(ContentView())
}
}
}
I have created a ObservableObject class of UserSettings that has loggedIn bool value. When the user taps on 'Log In' button in LogInView(), this bool value becomes true and a new view appears (TabbarView())
This is LogInView():
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image("2")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I know this error appears if I am updating the view while state is being modified (when typing in textfield). But I am not updating the view anywhere in the Log In screen. Then why this error occurs. Help will be appreciated!
This works for me, you don't even need to import Combine! When you use #Published, SwiftUI will automatically synthesize the objectWillChange subject, and will call send whenever the property is mutated. You can still call .send() manually if you need to, but in most cases you won't.
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
Excerpt from beta 5 release notes:
You can manually conform to ObservableObject by defining an
objectWillChange publisher that emits before the object changes.
However, by default, ObservableObject automatically synthesizes
objectWillChange and emits before any #Published properties change.
This is the full code that is working fine for me (both iPhone Xr and real device, iPad 6th Gen):
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
import SwiftUI
struct ContentView: View {
var body: some View {
StartView()
}
}
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(Text("LOGGED IN"))
} else {
return AnyView(LogInView())
}
}
}
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image(systemName: "2.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I guess this is a bug. This message you got is also happening on this simple view which filters out list entries by user input. Just typing fast in the text field causes this issue. If you enter the first character into the text field, the UI stuck for some time.
struct ContentView: View {
#State private var list: [String] = (0..<500).map { "Text \($0)" }
#State private var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
var body: some View {
VStack {
TextField("Search", text: $searchText)
List(filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
A workaround is to move the #State variables into a model. So this seems to be an issue with #State:
class Model: ObservableObject {
#Published var list: [String] = (0..<500).map { "Text \($0)" }
#Published var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
}
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
TextField("Search", text: $model.searchText)
List(model.filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
This may not be related to your issue, but in Xcode 11 Beta 4, Apple changed "didset" to "willset" and "didChange" to "willChange"
In Xcode 11 Beta 5, apple changed "willChange" to "objectWillChange".
Thus the StartView() should be:
class UserSettings: ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
willSet {
objectWillChange.send(())
}
}
}
Don't branch with if, use .opacity(_:)
#ViewBuilder
var body: some View {
// if settings.loggedIn {
TabbarView().opacity(settings.loggedIn ? 1 : 0)
// } else {
ContentView().opacity(settings.loggedIn ? 0 : 1)
// }
}

Error Cannot use instance member 'xxx' within property initializer

26-07-19
I'll update my code as I'm making progress watching the WWDC video's. My data model is:
struct Egg: Identifiable {
var id = UUID()
var thumbnailImage: String
var day: String
var date: String
var text: String
var imageDetail: String
var weight: Double
}
#if DEBUG
let testData = [
Egg(thumbnailImage: "Dag-1", day: "1.circle", date: "7 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-1", weight: 35.48),
Egg(thumbnailImage: "Dag-2", day: "2.circle", date: "8 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-2", weight: 35.23),
Egg(thumbnailImage: "Dag-3", day: "3.circle", date: "9 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-3", weight: 34.92)
Etc, etc
]
I've a TabbedView, a ContentView, a ContentDetail and a couple of other views (for settings etc). The code for the ContentView is:
struct ContentView : View {
var eggs : [Egg] = []
var body: some View {
NavigationView {
List(eggs) { egg in
EggCell(egg: egg)
}
.padding(.top, 10.0)
.navigationBarTitle(Text("Egg management"), displayMode: .inline)
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(eggs: testData)
}
}
#endif
struct EggCell : View {
let egg: Egg
var body: some View {
return NavigationLink(destination: ContentDetail(egg: egg)) {
ZStack {
HStack(spacing: 8.0) {
Image(egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.leading, -25)
.padding(.top, -15)
.padding(.bottom, -15)
.padding(.trailing, -25)
.frame(width: 85, height: 61)
VStack {
Image(systemName: egg.day)
.resizable()
.frame(width: 30, height: 22)
.padding(.leading, -82)
Spacer()
}
.padding(.leading)
VStack {
Text(egg.date)
.font(.headline)
.foregroundColor(Color.gray)
Text(egg.weight.clean)
.font(.title)
}
}
}
}
}
}
extension Double {
var clean: String {
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(format: "%.2f", self)
}
}
The code for the ContentDetail is:
struct ContentDetail : View {
let egg: Egg
#State private var photo = true
#State private var calculated = false
#Binding var weight: Double
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(egg.date)
.font(.title)
.fontWeight(.medium)
.navigationBarTitle(Text(egg.date), displayMode: .inline)
ZStack (alignment: .topLeading) {
Image(photo ? egg.imageDetail : egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.black)
.padding(.trailing, 0)
.tapAction { self.photo.toggle() }
VStack {
HStack {
Image(systemName: egg.day)
.resizable()
.padding(.leading, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
Spacer()
Image(systemName: photo ? "photo" : "wand.and.stars")
.resizable()
.padding(.trailing, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
}
Spacer()
HStack {
Image(systemName: "arrow.left.circle")
.resizable()
.padding(.leading, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
Spacer()
Image(systemName: "arrow.right.circle")
.resizable()
.padding(.trailing, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
}
}
}
Text("the weight is: \(egg.weight) gram")
.font(.headline)
.fontWeight(.bold)
ZStack {
RoundedRectangle(cornerRadius: 10)
.padding(.top, 45)
.padding(.bottom, 45)
.border(Color.gray, width: 5)
.opacity(0.1)
HStack {
Spacer()
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
Text(".")
.font(.largeTitle)
.fontWeight(.black)
.padding(.bottom, 10)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
Spacer()
}
}
Toggle(isOn: $calculated) {
Text(calculated ? "This weight is calculated." : "This weight is measured.")
}
Text(egg.text)
.lineLimit(2)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
Spacer()
}
.padding(6)
}
}
#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
static var previews: some View {
NavigationView { ContentDetail(egg: testData[0]) }
}
}
#endif
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
The compiler errors I'm still getting are:
in ContentView, beneath NavigationLink: Missing argument for
parameter 'weight' in call
in ContentDetail, at NavigationView: Missing argument for parameter
'weight' in call
in ContentDetail, after #endif: Missing argument for parameter
'weight' in call
25-07-19
The following code is part of a List detail view. The var 'weight' is coming from the List through a 'NavigationLink' statement. In this code I declare it as '35.48', but the NavigationLink fills in its real value.
I want to make an array [3, 5, 4, 8] with the compactMap statement. That works okay in Playground. The values go to 4 different pickers (within a HStack).
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = "\(weight)".compactMap { Int("\($0)") }
#State var digit1 = weightArray[0] // error
#State var digit2 = weightArray[1] // error
#State var digit3 = weightArray[2] // error
#State var digit4 = weightArray[3] // error
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
I get an error 'Cannot use instance member 'weightArray' within property initializer; property initializers run before 'self' is available'.
If I use the following code it works fine (for the first list element):
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = [3, 5, 4, 8]
#State var digit1 = 3
#State var digit2 = 5
#State var digit3 = 4
#State var digit4 = 8
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
What is the correct SwiftUI approach and why?
Indeed, a property initializer cannot refer to another property in the same container. You have to initialize your properties in an init instead.
struct ContentDetail: View {
var weight: Double
var weightArray: [Int]
#State var digit1: Int
#State var digit2: Int
#State var digit3: Int
#State var digit4: Int
init(weight: Double) {
self.weight = weight
weightArray = "\(weight)".compactMap { Int("\($0)") }
_digit1 = .init(initialValue: weightArray[0])
_digit2 = .init(initialValue: weightArray[1])
_digit3 = .init(initialValue: weightArray[2])
_digit4 = .init(initialValue: weightArray[3])
}
But I suspect you're breaking out the digits because you want to let the user edit them individually, like this:
If that's what you want, you should not have a separate #State property for each digit. Instead, weight should be a #Binding and it should have a separate mutable property for each digit.
First, extend Double to give you access to the digits:
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
Then, change ContentDetail's weight property to be a #Binding and get rid of the other properties:
struct ContentDetail: View {
#Binding var weight: Double
var body: some View {
HStack {
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
}
}
}
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
Here's the rest of the code needed to test this in a playground, which is how I generated that image above:
import PlaygroundSupport
struct TestView: View {
#State var weight: Double = 35.48
var body: some View {
VStack(spacing: 0) {
Text("Weight: \(weight)")
ContentDetail(weight: $weight)
.padding()
}
}
}
let host = UIHostingController(rootView: TestView())
host.preferredContentSize = .init(width: 320, height: 240)
PlaygroundPage.current.liveView = host